Compare commits

..

9 Commits

Author SHA1 Message Date
Justin Kolasa (from Dev Box)
f1484a72c8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-15 16:56:07 -04:00
Justin Kolasa (from Dev Box)
4444f66e91 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 10:02:56 -04:00
Justin Kolasa (from Dev Box)
2180eba0a0 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-14 09:47:45 -04:00
Justin Kolasa (from Dev Box)
ea1f0e9b0c Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 11:37:09 -04:00
Justin Kolasa (from Dev Box)
f66b78aaf8 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-13 09:28:03 -04:00
Justin Kolasa (from Dev Box)
30182ed503 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-08 08:55:42 -04:00
Justin Kolasa (from Dev Box)
9bf8cffd29 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-05 11:29:18 -04:00
Justin Kolasa (from Dev Box)
cb92bdb003 Merge branch 'master' of https://github.com/Azure/cosmos-explorer 2025-05-02 11:22:49 -04:00
Justin Kolasa (from Dev Box)
5f4ea0e614 master pull 2025-04-22 08:50:29 -04:00
447 changed files with 40437 additions and 345884 deletions

View File

@@ -1,20 +1 @@
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html
# Azure Test Environment Variables
# These are automatically set by the global-setup.ts script
# Azure resource group containing the test resources
DE_TEST_RESOURCE_GROUP=bchoudhury-e2e-testing
# Azure subscription ID for testing
DE_TEST_SUBSCRIPTION_ID=074d02eb-4d74-486a-b299-b262264d1536
# Prefix used for test resource names
DE_TEST_ACCOUNT_PREFIX=bchoudhury-e2e-
# Access token for NoSQL container copy operations
# This is generated automatically by the setup script
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=
# Optional: Set to true to enable debug logging during setup
DEBUG=false
DATA_EXPLORER_ENDPOINT=https://localhost:1234/hostedExplorer.html

View File

@@ -23,6 +23,8 @@ src/Common/MongoUtility.ts
src/Common/NotificationsClientBase.ts
src/Common/QueriesClient.ts
src/Common/Splitter.ts
src/Controls/Heatmap/Heatmap.test.ts
src/Controls/Heatmap/Heatmap.ts
src/Definitions/datatables.d.ts
src/Definitions/gif.d.ts
src/Definitions/globals.d.ts

View File

@@ -32,12 +32,6 @@ module.exports = {
extends: ["plugin:jest/recommended"],
plugins: ["jest"],
},
{
files: ["src/Explorer/ContainerCopy/**/*.{test,spec}.{ts,tsx}"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
},
},
],
rules: {
"no-console": ["error", { allow: ["error", "warn", "dir"] }],

View File

@@ -1,71 +0,0 @@
---
name: playwright-test-generator
description: 'Expert agent for generating robust Playwright tests for Cosmos DB. Supports DataExplorer and CopyJob flows. Required Inputs: <test-suite>, <test-name>, <test-file>, <seed-file>, <body>'
tools:
- azure-mcp/search
- playwright/browser_click
- playwright/browser_drag
- playwright/browser_evaluate
- playwright/browser_file_upload
- playwright/browser_handle_dialog
- playwright/browser_hover
- playwright/browser_navigate
- playwright/browser_press_key
- playwright/browser_select_option
- playwright/browser_snapshot
- playwright/browser_type
- playwright/browser_wait_for
- filesystem/read_file
- filesystem/write_file
- filesystem/list_directory
- filesystem/create_directory
model: Claude Sonnet 4
---
You are an expert Playwright Test Generator from given planner specs.
Your specialty is creating tests that match the project's high standards for frame navigation and FluentUI interaction. Your expertise includes functional testing using Playwright
# Pre-Generation Analysis
Before generating code, you MUST:
1. **Analyze Existing Specs:** Examine the provided `<seed-file>` and any referenced test files in the project to learn the specific coding standards, naming conventions, and import structures used.
2. **Identify the Flow Type:** Determine if the scenario is a **DataExplorer** flow or a **CopyJob** flow.
- **DataExplorer Flows:** Use `DataExplorer.open()` in the `beforeEach` or `beforeAll` setup logic.
- **CopyJob Flows:** Use `ContainerCopy.open()` in the `beforeEach` or `beforeAll` setup logic.
# Core Project Standards
- **Authentication:** Assume pre-flight authentication (az login/globalSetup) is complete. Do not generate login steps. The `global-setup` has been added into playwright.config.ts
- **Library Usage:** Prioritize common helper methods from `fx.ts`.
- **Page Hierarchy:**
- `frame`: Parent blade locator (used for overlays, dropdown lists, global portal buttons).
- `wrapper`: Child locator (used for the specific view content/form inputs).
- **Selector Strategy:** Use FluentUI-friendly locators: `getByRole` and `getByLabel`. Also prioritize `getByTestId` where available - check for `testIdAttribute` in playwright.config.ts as "data-test" attributes are configured as "data-testid".
# Step-by-Step Generation Workflow
1. **Plan Parsing:** Extract steps and verification logic from the planner spec.
2. **Setup:** Run `generator_setup_page` using the `<seed-file>`.
3. **Observation:** Use Playwright tools to execute steps. Pay close attention to which elements require the `frame` context vs. the `wrapper` context.
4. **Source Code Generation:**
- **File Naming:** Use fs-friendly names based on the scenario.
- **Structure:** Encapsulate in a `test.describe` matching the test plan group.
- **Code Style:** Match the patterns found in the analyzed existing files exactly.
- **Cleanup:** Ensure the test handles data cleanup to remain idempotent.
# Example Logic (Flow Detection)
```ts
// If CopyJob flow:
test.beforeEach(async ({ page }) => {
await ContainerCopy.open // Standardized entry point
});
(or)
test.beforeAll(async ({ page }) => {
await ContainerCopy.open // Standardized entry point
});
// If DataExplorer flow:
test.beforeEach(async ({ page }) => {
await DataExplorer.open // Standardized entry point
});
(or)
test.beforeAll(async ({ page }) => {
await DataExplorer.open // Standardized entry point
});

View File

@@ -1,61 +0,0 @@
---
name: playwright-test-healer
description: Use this agent when you need to debug and fix failing Playwright tests
tools:
- search
- edit
- playwright-test/browser_console_messages
- playwright-test/browser_evaluate
- playwright-test/browser_generate_locator
- playwright-test/browser_network_requests
- playwright-test/browser_snapshot
- playwright-test/test_debug
- playwright-test/test_list
- playwright-test/test_run
model: Claude Sonnet 4
mcp-servers:
playwright-test:
type: stdio
command: npx
args:
- playwright
- run-test-mcp-server
tools:
- "*"
---
You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix broken Playwright tests using a methodical approach.
Your workflow:
1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
2. **Debug failed tests**: For each failing test run `test_debug`.
3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
- Examine the error details
- Capture page snapshot to understand the context
- Analyze selectors, timing issues, or assertion failures
4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
- Element selectors that may have changed
- Timing and synchronization issues
- Data dependencies or test environment problems
- Application changes that broke test assumptions
5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
- Updating selectors to match current application state
- Fixing assertions and expected values
- Improving test reliability and maintainability
- For inherently dynamic data, utilize regular expressions to produce resilient locators
6. **Verification**: Restart the test after each fix to validate the changes
7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
Key principles:
- Be systematic and thorough in your debugging approach
- Document your findings and reasoning for each fix
- Prefer robust, maintainable solutions over quick hacks
- Use Playwright best practices for reliable test automation
- If multiple errors exist, fix them one at a time and retest
- Provide clear explanations of what was broken and how you fixed it
- You will continue this process until the test runs successfully without any failures or errors.
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
of the expected behavior.
- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
- Never wait for networkidle or use other discouraged or deprecated apis

View File

@@ -1,78 +0,0 @@
---
name: playwright-test-planner
description: Use this agent when you need to create comprehensive test plan for a web application or website
tools:
- search
- playwright-test/browser_click
- playwright-test/browser_close
- playwright-test/browser_console_messages
- playwright-test/browser_drag
- playwright-test/browser_evaluate
- playwright-test/browser_file_upload
- playwright-test/browser_handle_dialog
- playwright-test/browser_hover
- playwright-test/browser_navigate
- playwright-test/browser_navigate_back
- playwright-test/browser_network_requests
- playwright-test/browser_press_key
- playwright-test/browser_select_option
- playwright-test/browser_snapshot
- playwright-test/browser_take_screenshot
- playwright-test/browser_type
- playwright-test/browser_wait_for
- playwright-test/planner_setup_page
- playwright-test/planner_save_plan
model: Claude Sonnet 4
mcp-servers:
playwright-test:
type: stdio
command: npx
args:
- playwright
- run-test-mcp-server
tools:
- "*"
---
You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage planning.
You will:
1. **Navigate and Explore**
- Invoke the `planner_setup_page` tool once to set up page before using any other tools
- Explore the browser snapshot
- Do not take screenshots unless absolutely necessary
- Use `browser_*` tools to navigate and discover interface
- Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
2. **Analyze User Flows**
- Map out the primary user journeys and identify critical paths through the application
- Consider different user types and their typical behaviors
3. **Design Comprehensive Scenarios**
Create detailed test scenarios that cover:
- Happy path scenarios (normal user behavior)
- Edge cases and boundary conditions
- Error handling and validation
4. **Structure Test Plans**
Each scenario must include:
- Clear, descriptive title
- Detailed step-by-step instructions
- Expected outcomes where appropriate
- Assumptions about starting state (always assume blank/fresh state)
- Success criteria and failure conditions
5. **Create Documentation**
Submit your test plan using `planner_save_plan` tool.
**Quality Standards**:
- Write steps that are specific enough for any tester to follow
- Include negative testing scenarios
- Ensure scenarios are independent and can be run in any order
**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
professional formatting suitable for sharing with development and QA teams.

View File

@@ -164,8 +164,8 @@ jobs:
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
shardTotal: [20]
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
shardTotal: [16]
steps:
- uses: actions/checkout@v4
- name: Use Node.js 18.x
@@ -177,44 +177,9 @@ jobs:
- name: "Az CLI login"
uses: Azure/login@v2
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
# We can't use MSAL within playwright so we acquire tokens prior to running the tests
- name: "Acquire RBAC tokens for test accounts"
uses: azure/cli@v2
with:
azcliversion: latest
inlineScript: |
NOSQL_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_TESTACCOUNT_TOKEN"
echo NOSQL_TESTACCOUNT_TOKEN=$NOSQL_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_READONLY_TESTACCOUNT_TOKEN"
echo NOSQL_READONLY_TESTACCOUNT_TOKEN=$NOSQL_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-sql-containercopyonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN"
echo NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN=$NOSQL_CONTAINERCOPY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
TABLE_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-tables.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$TABLE_TESTACCOUNT_TOKEN"
echo TABLE_TESTACCOUNT_TOKEN=$TABLE_TESTACCOUNT_TOKEN >> $GITHUB_ENV
GREMLIN_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-gremlin.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$GREMLIN_TESTACCOUNT_TOKEN"
echo GREMLIN_TESTACCOUNT_TOKEN=$GREMLIN_TESTACCOUNT_TOKEN >> $GITHUB_ENV
CASSANDRA_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-cassandra.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$CASSANDRA_TESTACCOUNT_TOKEN"
echo CASSANDRA_TESTACCOUNT_TOKEN=$CASSANDRA_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_TESTACCOUNT_TOKEN"
echo MONGO_TESTACCOUNT_TOKEN=$MONGO_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO32_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo32.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO32_TESTACCOUNT_TOKEN"
echo MONGO32_TESTACCOUNT_TOKEN=$MONGO32_TESTACCOUNT_TOKEN >> $GITHUB_ENV
MONGO_READONLY_TESTACCOUNT_TOKEN=$(az account get-access-token --scope "https://github-e2etests-mongo-readonly.documents.azure.com/.default" -o tsv --query accessToken)
echo "::add-mask::$MONGO_READONLY_TESTACCOUNT_TOKEN"
echo MONGO_READONLY_TESTACCOUNT_TOKEN=$MONGO_READONLY_TESTACCOUNT_TOKEN >> $GITHUB_ENV
- name: List test files for shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list
- name: Run test shard ${{ matrix['shardIndex'] }} of ${{ matrix['shardTotal']}}
run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers=3
- name: Upload blob report to GitHub Actions Artifacts
@@ -255,4 +220,4 @@ jobs:
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14
retention-days: 14

View File

@@ -6,8 +6,8 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
schedule:
# Once every two hours
- cron: "0 */2 * * *"
# Once every hour
- cron: "0 15 * * *"
permissions:
id-token: write
@@ -27,7 +27,7 @@ jobs:
- name: "Az CLI login"
uses: azure/login@v1
with:
client-id: ${{ secrets.E2E_TESTS_CLIENT_ID }}
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
@@ -36,4 +36,4 @@ jobs:
with:
node-version: 18.x
- run: npm ci
- run: node utils/cleanupDBs.js
- run: node utils/cleanupDBs.js

View File

@@ -1,34 +0,0 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
# Customize this step as needed
- name: Build application
run: npx run build

1
.gitignore vendored
View File

@@ -21,4 +21,3 @@ GettingStarted-ignore*.ipynb
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/

6
.npmrc
View File

@@ -1,4 +1,6 @@
save-exact=true
registry=https://msdata.pkgs.visualstudio.com/_packaging/cosmosdbportal/npm/registry/
always-auth=true
allow-same-version=true
# Ignore peer dependency conflicts
force=true # TODO: Remove this when we update to React 17 or higher!
force=true # TODO: Remove this when we update to React 17 or higher!

23
.vscode/mcp.json vendored
View File

@@ -1,23 +0,0 @@
{
"servers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"],
"env": {
"ALLOWED_DIRECTORIES": "C:/Users/bchoudhury/MyProjects/cosmos-explorer"
}
},
"filesystem": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem@latest",
"C:/Users/bchoudhury/MyProjects/cosmos-explorer/test",
"C:/Users/bchoudhury/MyProjects/cosmos-explorer/src"
]
}
},
"inputs": []
}

View File

@@ -24,5 +24,6 @@
"source.organizeImports": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"sarif-viewer.connectToGithubCodeScanning": "off"
}

View File

@@ -19,6 +19,6 @@
</frameworkAssemblies>
</metadata>
<files>
<file src="**\*" exclude="obj\**\*" target="content"/>
<file src="**\*" target="content"/>
</files>
</package>

View File

@@ -1,4 +1,5 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled": true,
"isPhoenixEnabled": true
}
}

View File

@@ -1,4 +1,5 @@
{
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isPhoenixEnabled": false
}
"JUNO_ENDPOINT": "https://tools.cosmos.azure.com",
"isTerminalEnabled" : false,
"isPhoenixEnabled" : false
}

View File

@@ -1 +0,0 @@
<svg id="uuid-adbdae8e-5a41-46d1-8c18-aa73cdbfee32" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18"><path d="m0,2.7v12.6c0,1.491,1.209,2.7,2.7,2.7h12.6c1.491,0,2.7-1.209,2.7-2.7V2.7c0-1.491-1.209-2.7-2.7-2.7H2.7C1.209,0,0,1.209,0,2.7ZM10.8,0v3.6c0,3.976,3.224,7.2,7.2,7.2h-3.6c-3.976,0-7.199,3.222-7.2,7.198v-3.598c0-3.976-3.224-7.2-7.2-7.2h3.6c3.976,0,7.2-3.224,7.2-7.2Z" fill="#000000" stroke-width="0" /></svg>

Before

Width:  |  Height:  |  Size: 443 B

View File

@@ -1,17 +0,0 @@
<svg width="96" height="104" viewBox="0 0 96 104" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" d="M80.5008 81.2203L41.2637 58.2012L35.7705 61.9941L74.6152 84.6208L80.5008 81.2203Z" fill="#AAAAAA"/>
<path opacity="0.2" d="M60.2283 92.5992L20.9912 69.5801L15.498 73.373L54.3428 95.9997L60.2283 92.5992Z" fill="#AAAAAA"/>
<path d="M63.7596 30.9969L74.8768 37.4057L74.746 82.1359L35.7705 59.7708L35.9013 3.00781L63.7596 19.095V30.9969Z" fill="#C9C9C9"/>
<path d="M35.9014 3.00818L41.0022 0L68.8605 16.0872L63.7597 19.0954L35.9014 3.00818Z" fill="#AAAAAA"/>
<path d="M74.8769 37.4067L79.9777 34.5293L79.8469 79.2596L74.7461 82.2677L74.8769 37.4067Z" fill="#AAAAAA"/>
<path d="M43.4872 42.245L54.6043 48.6537L54.4735 93.384L15.498 71.0188L15.6288 14.2559L43.4872 30.3431V42.245Z" fill="#F4F4F4"/>
<path d="M15.6289 14.2562L20.7297 11.248L48.5881 27.3352L43.4872 30.3434L15.6289 14.2562Z" fill="#DCDCDC"/>
<path d="M54.6044 48.6547L59.7052 45.7773L59.5745 90.5076L54.4736 93.5158L54.6044 48.6547Z" fill="#DCDCDC"/>
<path d="M63.7598 19.0961L68.8606 16.0879L79.9778 34.5293L74.8769 37.4067L63.7598 19.0961Z" fill="#C9C9C9"/>
<path d="M63.7598 19.0957L74.8769 37.4063L63.7598 30.9976V19.0957Z" fill="#DCDCDC"/>
<path d="M43.4873 30.3441L48.5881 27.3359L59.7053 45.7774L54.6045 48.6548L43.4873 30.3441Z" fill="#F4F4F4"/>
<path d="M43.4873 30.3438L54.6045 48.6544L43.4873 42.2457V30.3438Z" fill="#C9C9C9"/>
<path d="M46.8751 52.4595V55.9693L23.2275 42.1367V38.627L46.8751 52.4595Z" fill="#C9C9C9"/>
<path d="M46.8751 59.0658V62.5756L23.2275 48.6914V45.1816L46.8751 59.0658Z" fill="#C9C9C9"/>
<path d="M46.8751 65.3621V68.8719L23.2275 54.9877V51.6328L46.8751 65.3621Z" fill="#C9C9C9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M4 4c0-1.1.9-2 2-2h3.59c.4 0 .78.16 1.06.44l3.91 3.91c.28.28.44.67.44 1.06V14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4Zm2-1a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 9 6.5V3H6Zm4 .2v3.3c0 .28.22.5.5.5h3.3L10 3.2ZM17 9a1 1 0 0 0-1-1v6a3 3 0 0 1-3 3H6a1 1 0 0 0 1 1h6.06A3.94 3.94 0 0 0 17 14.06V9Z" />
</svg>

Before

Width:  |  Height:  |  Size: 439 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.85032 3.0153C10.4276 3.21621 12.4563 5.37119 12.4563 8C12.4563 10.7614 10.2177 13 7.45629 13C5.70158 13 4.15722 12.0961 3.26465 10.7271C4.66791 10.3479 6.58077 9.42526 7.42438 7.17555C7.97709 5.70162 8.00857 4.23763 7.85032 3.0153ZM13.4563 8C13.4563 4.68629 10.77 2 7.45629 2C7.38577 2 7.31552 2.00122 7.24555 2.00364C7.09984 2.00867 6.96358 2.07706 6.87247 2.19089C6.78136 2.30471 6.74447 2.45263 6.77147 2.59591C7.00024 3.81021 7.05064 5.32413 6.48805 6.82444C5.68804 8.95787 3.68609 9.66359 2.41062 9.89533C2.25698 9.92325 2.1252 10.0213 2.05438 10.1605C1.98356 10.2997 1.98182 10.4639 2.04969 10.6046C3.01873 12.6128 5.07502 14 7.45629 14C10.77 14 13.4563 11.3137 13.4563 8Z" fill="#0078d4" />
</svg>

Before

Width:  |  Height:  |  Size: 814 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 1.33C8.276 1.33 8.5 1.554 8.5 1.83V2.83C8.5 3.106 8.276 3.33 8 3.33C7.724 3.33 7.5 3.106 7.5 2.83V1.83C7.5 1.554 7.724 1.33 8 1.33ZM8 11.33C9.841 11.33 11.33 9.841 11.33 8C11.33 6.159 9.841 4.67 8 4.67C6.159 4.67 4.67 6.159 4.67 8C4.67 9.841 6.159 11.33 8 11.33ZM8 10.33C6.711 10.33 5.67 9.289 5.67 8C5.67 6.711 6.711 5.67 8 5.67C9.289 5.67 10.33 6.711 10.33 8C10.33 9.289 9.289 10.33 8 10.33ZM14.17 8.5C14.446 8.5 14.67 8.276 14.67 8C14.67 7.724 14.446 7.5 14.17 7.5H13.17C12.894 7.5 12.67 7.724 12.67 8C12.67 8.276 12.894 8.5 13.17 8.5H14.17ZM8 12.67C8.276 12.67 8.5 12.894 8.5 13.17V14.17C8.5 14.446 8.276 14.67 8 14.67C7.724 14.67 7.5 14.446 7.5 14.17V13.17C7.5 12.894 7.724 12.67 8 12.67ZM2.83 8.5C3.106 8.5 3.33 8.276 3.33 8C3.33 7.724 3.106 7.5 2.83 7.5H1.83C1.554 7.5 1.33 7.724 1.33 8C1.33 8.276 1.554 8.5 1.83 8.5H2.83ZM2.813 2.813C3.009 2.617 3.325 2.617 3.521 2.813L4.521 3.813C4.717 4.009 4.717 4.325 4.521 4.521C4.325 4.717 4.009 4.717 3.813 4.521L2.813 3.521C2.617 3.325 2.617 3.009 2.813 2.813ZM3.521 13.187C3.325 13.383 3.009 13.383 2.813 13.187C2.617 12.991 2.617 12.675 2.813 12.479L3.813 11.479C4.009 11.283 4.325 11.283 4.521 11.479C4.717 11.675 4.717 11.991 4.521 12.187L3.521 13.187ZM13.187 2.813C12.991 2.617 12.675 2.617 12.479 2.813L11.479 3.813C11.283 4.009 11.283 4.325 11.479 4.521C11.675 4.717 11.991 4.717 12.187 4.521L13.187 3.521C13.383 3.325 13.383 3.009 13.187 2.813ZM12.479 13.187C12.675 13.383 12.991 13.383 13.187 13.187C13.383 12.991 13.383 12.675 13.187 12.479L12.187 11.479C11.991 11.283 11.675 11.283 11.479 11.479C11.283 11.675 11.283 11.991 11.479 12.187L12.479 13.187Z" fill="#0078d4" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0C8.73438 0 9.44271 0.0960961 10.125 0.288288C10.8073 0.48048 11.4427 0.758091 12.0312 1.12112C12.6198 1.48415 13.1589 1.91124 13.6484 2.4024C14.138 2.89356 14.5573 3.44611 14.9062 4.06006C15.2552 4.67401 15.5234 5.32799 15.7109 6.02202C15.8984 6.71605 15.9948 7.44211 16 8.2002C16 9.08108 15.8698 9.92993 15.6094 10.7467C15.349 11.5636 14.9766 12.3136 14.4922 12.997C14.0078 13.6803 13.4323 14.2783 12.7656 14.7908C12.099 15.3033 11.3542 15.701 10.5312 15.984C10.5156 15.9893 10.4922 15.992 10.4609 15.992C10.4297 15.992 10.4062 15.9947 10.3906 16C10.2656 16 10.1667 15.9626 10.0938 15.8879C10.0208 15.8131 9.98438 15.7144 9.98438 15.5916V14.4705C9.98438 14.1021 9.98698 13.7257 9.99219 13.3413C9.99219 13.0691 9.95312 12.7941 9.875 12.5165C9.79688 12.2389 9.65625 12.0067 9.45312 11.8198C10.0573 11.7504 10.5859 11.625 11.0391 11.4434C11.4922 11.2619 11.8724 11.0057 12.1797 10.6747C12.487 10.3437 12.7161 9.94328 12.8672 9.47347C13.0182 9.00367 13.0964 8.43777 13.1016 7.77578C13.1016 7.35936 13.0339 6.96697 12.8984 6.5986C12.763 6.23023 12.5573 5.88856 12.2812 5.57357C12.3385 5.42409 12.3802 5.26927 12.4062 5.10911C12.4323 4.94895 12.4453 4.78879 12.4453 4.62863C12.4453 4.42042 12.4245 4.21488 12.3828 4.01201C12.3411 3.80914 12.2812 3.60627 12.2031 3.4034C12.1771 3.39273 12.1484 3.38739 12.1172 3.38739C12.0859 3.38739 12.0573 3.38739 12.0312 3.38739C11.8646 3.38739 11.6901 3.41408 11.5078 3.46747C11.3255 3.52085 11.1458 3.59026 10.9688 3.67568C10.7917 3.76109 10.6172 3.85452 10.4453 3.95596C10.2734 4.05739 10.125 4.15349 10 4.24424C9.34896 4.05739 8.68229 3.96396 8 3.96396C7.31771 3.96396 6.65104 4.05739 6 4.24424C5.86979 4.15349 5.72135 4.05739 5.55469 3.95596C5.38802 3.85452 5.21615 3.76376 5.03906 3.68368C4.86198 3.6036 4.67969 3.5342 4.49219 3.47548C4.30469 3.41675 4.13021 3.38739 3.96875 3.38739H3.88281C3.85156 3.38739 3.82292 3.39273 3.79688 3.4034C3.72396 3.60093 3.66667 3.80113 3.625 4.004C3.58333 4.20687 3.5599 4.41508 3.55469 4.62863C3.55469 4.78879 3.56771 4.94895 3.59375 5.10911C3.61979 5.26927 3.66146 5.42409 3.71875 5.57357C3.44271 5.88322 3.23698 6.22222 3.10156 6.59059C2.96615 6.95896 2.89844 7.35402 2.89844 7.77578C2.89844 8.42709 2.97396 8.99032 3.125 9.46547C3.27604 9.94061 3.50521 10.341 3.8125 10.6667C4.11979 10.9923 4.5 11.2513 4.95312 11.4434C5.40625 11.6356 5.9349 11.7638 6.53906 11.8278C6.38802 11.9666 6.27344 12.1321 6.19531 12.3243C6.11719 12.5165 6.0625 12.7167 6.03125 12.9249C5.89062 12.9943 5.74219 13.0477 5.58594 13.0851C5.42969 13.1225 5.27344 13.1411 5.11719 13.1411C4.78385 13.1411 4.50781 13.0611 4.28906 12.9009C4.07031 12.7407 3.875 12.5219 3.70312 12.2442C3.64062 12.1428 3.5651 12.0414 3.47656 11.9399C3.38802 11.8385 3.29167 11.7477 3.1875 11.6677C3.08333 11.5876 2.97135 11.5235 2.85156 11.4755C2.73177 11.4274 2.60677 11.4007 2.47656 11.3954H2.38281C2.34115 11.3954 2.30208 11.4034 2.26562 11.4194C2.22917 11.4354 2.19271 11.4515 2.15625 11.4675C2.11979 11.4835 2.10417 11.5102 2.10938 11.5475C2.10938 11.6116 2.14583 11.673 2.21875 11.7317C2.29167 11.7905 2.35156 11.8385 2.39844 11.8759L2.42188 11.8919C2.53646 11.9826 2.63542 12.0681 2.71875 12.1481C2.80208 12.2282 2.88021 12.3163 2.95312 12.4124C3.02604 12.5085 3.08594 12.6099 3.13281 12.7167C3.17969 12.8235 3.23958 12.9489 3.3125 13.0931C3.48958 13.5095 3.73698 13.8111 4.05469 13.998C4.3724 14.1849 4.75521 14.2809 5.20312 14.2863C5.33854 14.2863 5.47396 14.2783 5.60938 14.2623C5.74479 14.2462 5.88021 14.2222 6.01562 14.1902V15.5836C6.01562 15.7117 5.97917 15.8131 5.90625 15.8879C5.83333 15.9626 5.73177 16 5.60156 16H5.53906C5.51302 16 5.48958 15.9947 5.46875 15.984C4.65104 15.7117 3.90625 15.3193 3.23438 14.8068C2.5625 14.2943 1.98698 13.6937 1.50781 13.005C1.02865 12.3163 0.658854 11.5636 0.398438 10.7467C0.138021 9.92993 0.00520833 9.08108 0 8.2002C0 7.44745 0.09375 6.72139 0.28125 6.02202C0.46875 5.32266 0.739583 4.67134 1.09375 4.06807C1.44792 3.4648 1.86458 2.91225 2.34375 2.41041C2.82292 1.90858 3.36198 1.47881 3.96094 1.12112C4.5599 0.76343 5.19792 0.488488 5.875 0.296296C6.55208 0.104104 7.26042 0.00533867 8 0Z" fill="#000000"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 7.5C4 5.567 5.567 4 7.5 4C8.5 4 9.4 4.4 10 5.1C9.5 4.8 8.9 4.6 8.3 4.6C6.8 4.6 5.6 5.8 5.6 7.3C5.6 8.8 6.8 10 8.3 10C8.9 10 9.5 9.8 10 9.5C9.4 10.2 8.5 10.6 7.5 10.6C5.567 10.6 4 9.033 4 7.1V7.5Z" fill="#0078D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 328 B

View File

@@ -1,11 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 2C6 1.44772 6.44772 1 7 1C7.55228 1 8 1.44772 8 2C8 2.55228 7.55228 3 7 3C6.44772 3 6 2.55228 6 2Z" fill="#0078D4"/>
<path d="M6 13C6 12.4477 6.44772 12 7 12C7.55228 12 8 12.4477 8 13C8 13.5523 7.55228 14 7 14C6.44772 14 6 13.5523 6 13Z" fill="#0078D4"/>
<path d="M1 7C1 6.44772 1.44772 6 2 6C2.55228 6 3 6.44772 3 7C3 7.55228 2.55228 8 2 8C1.44772 8 1 7.55228 1 7Z" fill="#0078D4"/>
<path d="M12 7C12 6.44772 12.4477 6 13 6C13.5523 6 14 6.44772 14 7C14 7.55228 13.5523 8 13 8C12.4477 8 12 7.55228 12 7Z" fill="#0078D4"/>
<path d="M2.63604 3.63604C3.02656 3.24551 3.65973 3.24551 4.05025 3.63604C4.44078 4.02656 4.44078 4.65973 4.05025 5.05025C3.65973 5.44078 3.02656 5.44078 2.63604 5.05025C2.24551 4.65973 2.24551 4.02656 2.63604 3.63604Z" fill="#0078D4"/>
<path d="M10.9497 9.94975C11.3403 9.55922 11.9734 9.55922 12.364 9.94975C12.7545 10.3403 12.7545 10.9734 12.364 11.364C11.9734 11.7545 11.3403 11.7545 10.9497 11.364C10.5592 10.9734 10.5592 10.3403 10.9497 9.94975Z" fill="#0078D4"/>
<path d="M10.9497 5.05025C10.5592 4.65973 10.5592 4.02656 10.9497 3.63604C11.3403 3.24551 11.9734 3.24551 12.364 3.63604C12.7545 4.02656 12.7545 4.65973 12.364 5.05025C11.9734 5.44078 11.3403 5.44078 10.9497 5.05025Z" fill="#0078D4"/>
<path d="M2.63604 11.364C2.24551 10.9734 2.24551 10.3403 2.63604 9.94975C3.02656 9.55922 3.65973 9.55922 4.05025 9.94975C4.44078 10.3403 4.44078 10.9734 4.05025 11.364C3.65973 11.7545 3.02656 11.7545 2.63604 11.364Z" fill="#0078D4"/>
<circle cx="7.5" cy="7.5" r="2.5" fill="#0078D4"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -128,7 +128,7 @@
@provisionDatabaseThroughputInfo: 200px;
//tabs container
@ActiveTabHeight: 32px;
@ActiveTabHeight: 31px;
@ActiveTabWidth: 141px;
@TabsHeight: 30px;
@TabsWidth: 140px;
@@ -237,11 +237,11 @@
*********************************************************************************************/
.hover() {
background-color: var(--colorNeutralBackground1Hover);
background-color: @AccentLight;
}
.active() {
background-color: var(--colorNeutralBackground1Hover);
background-color: @AccentExtra;
}
.focus() {

View File

@@ -406,11 +406,7 @@ body {
width: 440px;
min-height: 565px;
}
.dataExplorerLoaderforcopyJobs{
width: 100%;
min-height: 565px;
right: 0;
}
.dataExplorerTabLoaderContainer {
left: initial;
top: initial;
@@ -1744,7 +1740,7 @@ input::-webkit-calendar-picker-indicator {
flex: 1;
padding-left: 34px;
padding-right: 34px;
color: var(--colorNeutralForeground1);
color: @BaseDark;
overflow-y: auto;
overflow-x: auto;
margin: (2 * @MediumSpace) 0px;
@@ -1753,6 +1749,7 @@ input::-webkit-calendar-picker-indicator {
.contextual-pane .panelMainContent {
padding-left: 34px;
padding-right: 34px;
color: @BaseDark;
margin: (2 * @MediumSpace) 0px;
}
@@ -1917,8 +1914,7 @@ input::-webkit-calendar-picker-indicator::after {
}
.nav-tabs-margin {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
background-color: #f2f2f2;
.nav-tabs {
display: flex;
@@ -1926,19 +1922,11 @@ input::-webkit-calendar-picker-indicator::after {
align-items: flex-end;
height: 100%;
margin-bottom: -0.5px;
background-color: var(--colorNeutralBackground1Selected);
li {
// Override the bootstrap defaults here to align with our layout constants.
margin-bottom: 0px;
height: 32px;
&:hover {
background-color: var(--colorNeutralBackground1Hover);
}
&:active {
background-color: var(--colorNeutralBackground1Pressed);
}
}
}
}
@@ -1952,9 +1940,8 @@ input::-webkit-calendar-picker-indicator::after {
.nav.nav-tabs.qslevel > li > a:hover {
border: none;
border-radius: 0;
background-color: var(--colorNeutralBackground1Selected);
background-color: transparent !important;
border-color: transparent;
color: var(--colorNeutralForeground1);
}
.numbersize {
@@ -2389,8 +2376,6 @@ a:link {
display: flex;
flex-direction: column;
min-width: 0; // This prevents it to grow past the parent's width if its content is too wide
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.tabs {
@@ -2646,16 +2631,14 @@ a:link {
}
.tabPanesContainer {
display: flex;
flex-grow: 1;
overflow: hidden;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
}
.tabs-container {
height: 100%;
width: 100%;
overflow-y: auto;
}
.paddingspan4 {
@@ -2672,18 +2655,24 @@ a:link {
.nav-tabs > li.active > .tabNavContentContainer,
.nav-tabs > li.active > .tabNavContentContainer:focus,
.nav-tabs > li.active > .tabNavContentContainer:hover {
color: var(--colorNeutralForeground1);
color: #555;
cursor: default;
background-color: var(--colorNeutralBackground1);
border-color: var(--colorNeutralStroke1);
background-color: @BaseLight;
border-color: @BaseMedium;
border-bottom-color: @BaseLight;
border-style: solid;
border-width: 1px;
height: @ActiveTabHeight;
width: @ActiveTabWidth;
}
.nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid var(--colorCompoundBrandBackground);
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
font-weight: bolder;
border-bottom: 2px solid rgba(0, 120, 212, 1);
}
.nav-tabs > li.active:focus > .tabNavContentContainer {
.focus();
}
.tabNavContentContainer {
@@ -2692,7 +2681,7 @@ a:link {
justify-content: space-between;
border-radius: 2px 2px 0 0;
padding: @DefaultSpace 0px @SmallSpace 0px;
color: var(--colorNeutralForeground1);
color: @BaseHigh;
width: @TabsWidth;
text-align: center;
position: relative;
@@ -2700,21 +2689,19 @@ a:link {
&:hover {
text-decoration: none;
background-color: var(--colorNeutralBackground1Hover);
border-color: transparent;
background-color: @BaseMediumLow;
border-color: @BaseMediumLow;
}
&:active {
background-color: var(--colorNeutralBackground1Pressed);
background-color: @BaseMediumLow;
}
.tab_Content {
.flex-display();
width: @TabsWidth;
border-right: @ButtonBorderWidth solid var(--colorNeutralStroke1);
border-right: @ButtonBorderWidth solid @BaseMedium;
white-space: nowrap;
color: var(--colorNeutralForeground1);
.contentWrapper {
.flex-display();
width: @ContentWrapper;
@@ -2736,8 +2723,9 @@ a:link {
background-image: url(../images/error_no_outline.svg);
background-repeat: no-repeat;
background-position: center;
background-size: 3px;
display: block;
margin: 4px 0px 0px 6px;
margin: 1px 0px 0px 6px;
}
}
@@ -2762,60 +2750,39 @@ a:link {
.loadingIcon {
width: @LoadingErrorIconSize;
height: @LoadingErrorIconSize;
margin-top: 1px;
}
.warningIconContainer {
width: @ErrorIconContainer;
height: @ErrorIconContainer;
margin-top: 1px;
margin: 0px 0px @SmallSpace @SmallSpace;
}
}
.tabNavText {
margin-left: @SmallSpace;
margin-right: 2px;
color: var(--colorNeutralForeground1);
color: @BaseDark;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
flex-grow: 1;
font-size: 12px;
}
}
.tabIconSection {
width: 29px;
position: relative;
padding-top: 2px;
.cancelButton {
padding: 0px @SmallSpace 0px @SmallSpace;
color: var(--colorNeutralForeground1);
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&:hover {
background-color: var(--colorNeutralBackground1Hover);
color: var(--colorNeutralForeground1);
.hover();
}
&:focus {
background-color: var(--colorNeutralBackground1Pressed);
color: var(--colorNeutralForeground1);
.focus();
}
&:active {
background-color: var(--colorNeutralBackground1Pressed);
color: var(--colorNeutralForeground1);
}
&::before {
content: "×";
font-size: 16px;
line-height: 1;
.active();
}
}
}
@@ -2902,7 +2869,6 @@ a:link {
z-index: 1000;
overflow-y: auto;
overflow-x: clip;
min-height: fit-content;
}
.uniqueIndexesContainer {
@@ -3170,12 +3136,3 @@ a:link {
.sidebarContainer {
height: 100%;
}
.close-Icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
cursor: pointer;
}

View File

@@ -7,7 +7,6 @@ html {
body {
font-family: @FabricFont;
background-color: #f5f5f5;
--colorCompoundBrandBackground: @FabricAccentMedium;
}
a {
@@ -42,7 +41,7 @@ a:focus {
}
.nav-tabs-margin {
padding-top: 0px;
padding-top: 5px;
background-color: #ffffff;
}
@@ -69,20 +68,17 @@ a:focus {
}
.nav-tabs > li > .tabNavContentContainer > .tab_Content:hover {
border-bottom: none;
border-bottom: 2px solid #e0e0e0;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content,
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content:hover {
border-bottom: none;
border-bottom: 2px solid @FabricAccentMedium;
}
.nav-tabs > li.active > .tabNavContentContainer > .tab_Content > .contentWrapper > .tabNavText {
border-bottom: 0px none transparent;
}
.nav-tabs > li.active .contentWrapper .tabNavText {
border-bottom: 2px solid @FabricAccentMedium;
}
.tabNavContentContainer {
padding: @SmallSpace 0px @SmallSpace 0px;
@@ -218,7 +214,6 @@ a:focus {
.tabPanesContainer {
overflow: auto !important;
display: flex;
}
.tabs-container {

View File

@@ -1,227 +1,211 @@
@import "./Common/Constants";
.dirty {
border: 1px solid #9b4f96;
border: 1px solid #9b4f96;
}
.dirty:focus {
outline: 1px solid #9b4f96;
outline: 1px solid #9b4f96;
}
.tabForm {
padding: 12px 20px 20px 20px;
margin-left: 3px;
padding: 12px 20px 20px 20px;
margin-left: 3px;
}
.storedTabForm {
padding-top: @LargeSpace;
padding-top: @LargeSpace;
}
.scaleSettingScrollable {
overflow-y: auto;
overflow-x: hidden;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
height:100%;
}
.disableFocusDefaults[tabindex] {
&:focus {
outline: none;
}
&:focus {
outline: none;
}
&:active {
outline: none;
}
&:active {
outline: none;
}
}
.indexingPolicyEditor {
width: 100%;
height: calc(~"100vh - 400px");
width: 100%;
height: calc(~"100vh - 400px");
}
.scaleDivison {
padding: @MediumSpace 0px @DefaultSpace 0px;
padding: @MediumSpace 0px @DefaultSpace 0px;
}
.scaleSettingTitle {
font-size: 14px;
cursor: pointer;
font-size: 14px;
cursor: pointer;
}
.autoScaleThroughputTitle {
margin-bottom: @SmallSpace;
margin-bottom: @SmallSpace;
}
.autoScaleDescription {
margin-top: 6px;
margin-bottom: @SmallSpace;
margin-top: 6px;
margin-bottom: @SmallSpace;
}
.ssExpandCollapseIcon {
width: 10px;
height: 10px;
width: 10px;
height: 10px;
}
.ssCollapseIcon {
margin-bottom: 5px;
margin-bottom: 5px;
}
.ssTextAllignment {
padding-left: 19px;
padding-left: 19px;
}
.throughputStorageBlock {
border-top: 1px solid #bbb;
border-bottom: 1px solid #bbb;
background-color: #ccc;
padding-left: 10px;
width: 315px;
border-top: 1px solid #bbb;
border-bottom: 1px solid #bbb;
background-color: #ccc;
padding-left: 10px;
width: 315px;
}
.storageCapacityTitle {
padding: @LargeSpace 0px;
padding: @LargeSpace 0px;
}
.throughputStorageValue {
font-size: 12px;
font-size: 12px;
}
.estimatedCost,
.largePartitionKeyEnabled {
padding: @SmallSpace 0px @LargeSpace;
.estimatedCost, .largePartitionKeyEnabled {
padding: @SmallSpace 0px @LargeSpace;
}
.storagePadding {
padding-top: 6px;
padding-bottom: 14px;
padding-top: 6px;
padding-bottom: 14px;
}
.dirtyTextbox {
width: 176px;
margin-top: 7px;
padding-left: 5px;
width: 176px;
margin-top: 7px;
padding-left: 5px;
}
.formTitleFirst {
padding: @DefaultSpace (2 * @MediumSpace);
padding: @DefaultSpace (2 * @MediumSpace);
}
.formTitleTextbox {
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
padding: 0px 0px @DefaultSpace (2 * @MediumSpace);
}
.formTree {
border: 1px solid var(--colorNeutralStroke1);
color: var(--colorNeutralForeground1);
background-color: var(--colorNeutralBackground1);
padding: 0px 12px 1px 8px;
border: 1px solid #969696;
color: #393939;
padding: 0px 12px 1px 8px;
}
.formTree:hover {
border: 1px solid var(--colorNeutralStroke1Hover);
background-color: var(--colorNeutralBackground1Hover);
}
.formTree::placeholder {
color: var(--colorNeutralForeground2);
opacity: 1;
border: 1px solid #969696;
background-color: #e6f8fe;
}
.formTree:active {
border: 1px solid var(--colorNeutralStroke1Pressed);
background-color: var(--colorNeutralBackground1Pressed);
border: 1px solid #1ebbee;
}
.scaleForm {
padding-left: 8px;
color: @BaseDark;
border: 1px solid #969696;
min-width: @ScaleFormWidth;
padding-left: 8px;
color: @BaseDark;
border: 1px solid #969696;
min-width: @ScaleFormWidth;
&:hover {
background-color: #e6f8fe;
}
&:hover {
background-color: #e6f8fe;
}
}
.formTitle {
margin-top: 16px;
margin-bottom: 4px;
margin-top: 16px;
margin-bottom: 4px;
}
.spUdfTriggerHeader {
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
padding: @DefaultSpace 0px @SmallSpace (2 * @MediumSpace);
}
.storedUdfTriggerEditor {
width: 100%;
height: 100%;
width: 100%;
height: 100%;
}
.unselectedRadio {
background-color: white;
border-color: #eee !important;
color: black !important;
background-color: white;
border-color: #EEE!important;
color: black!important;
}
.disabledRadio {
background-color: #a19f9d;
border-color: #eee !important;
color: white !important;
background-color: #A19F9D;
border-color: #EEE!important;
color: white!important;
}
.selectedRadio {
background-color: @AccentMediumHigh;
border-color: #eee !important;
color: white !important;
cursor: pointer;
background-color: @AccentMediumHigh;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
}
.selectedRadio:hover {
background-color: @AccentMediumHigh;
border-color: #eee !important;
color: white !important;
cursor: pointer;
background-color: @AccentMediumHigh;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
}
.selectedRadio:active {
background-color: #0072c6;
border-color: #eee !important;
color: white !important;
cursor: pointer;
border: 1px solid #0072c6;
background-color: #0072c6;
border-color: #EEE!important;
color: white!important;
cursor: pointer;
border: 1px solid #0072c6;
}
.selectedRadio.dirty {
background-color: #9b4f96;
background-color: #9b4f96;
}
.tabs {
margin: 0;
margin: 0;
}
.formReadOnly {
background-color: #ddd;
border: 1px solid #969696;
min-width: 184px;
padding-left: 8px;
background-color: #ddd;
border: 1px solid #969696;
min-width: 184px;
padding-left: 8px;
}
.migration:disabled {
background-color: #ccc;
background-color: #ccc;
}
.trigger-field {
width: 40%;
margin-top: 10px;
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
width: 40%;
margin-top: 10px
}
.trigger-field input::placeholder {
color: var(--colorNeutralForeground3);
opacity: 1;
}
.trigger-form {
background-color: var(--colorNeutralBackground1);
color: var(--colorNeutralForeground1);
padding: 10px 30px;
}
padding: 10px 30px 10px 30px;
}

View File

@@ -255,7 +255,7 @@ body {
flex: 1;
padding-left: 34px;
padding-right: 34px;
color: var(--colorNeutralForeground1);
color: @BaseDark;
overflow-y: auto;
overflow-x: hidden;
margin: (2 * @MediumSpace) 0px;

270
less/tree.less Normal file
View File

@@ -0,0 +1,270 @@
@import "./Common/Constants";
.resourceTree {
height: 100%;
flex: 0 0 auto;
.main {
height: 100%;
}
}
.resourceTreeScroll {
height: 100%;
display: flex;
overflow-y: auto;
overflow-x: hidden;
padding-right: 10px;
}
.userSelectNone {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.treeHovermargin {
margin-left: 16px;
}
.highlight {
padding: @SmallSpace 2px;
outline: 0;
&:hover {
.hover();
}
&:active {
.active();
}
&:focus {
.focus();
}
}
.contextmenushowing {
background-color: #eee;
}
.collectionstree {
width: 100%;
margin-top: @DefaultSpace;
.databaseList {
list-style-type: none;
padding-left: 0px;
.collectionList {
padding-left: (2 * @MediumSpace);
}
.collectionChildList {
padding-left: @LargeSpace;
}
.databaseDocuments {
padding-left: (5 * @MediumSpace);
}
}
}
.pointerCursor {
cursor: pointer;
}
.menuEllipsis {
padding-right: 6px;
font-weight: bold;
font-size: 18px;
position: relative;
top: -5px;
left: 0px;
float: right;
display: none;
padding-left: 6px !important;
line-height: @TreeLineHeight;
}
.databaseMenu {
.flex-display();
}
.databaseMenu:hover .menuEllipsis,
.databaseMenu:focus .menuEllipsis {
display: block;
}
.databaseCollChildTextOverflow {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
flex: 1;
}
.collectionMenu {
.flex-display();
}
.collectionMenu:hover .menuEllipsis,
.collectionMenu:focus .menuEllipsis {
display: block;
}
.documentsMenu:hover .menuEllipsis,
.documentsMenu:focus .menuEllipsis {
display: block;
}
.treeChildMenu {
display: flex;
}
.storedProcedureMenu:hover .menuEllipsis,
.storedProcedureMenu:focus .menuEllipsis {
display: block;
}
.childMenu {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: (6 * @MediumSpace);
width: 100%;
}
.storedChildMenu:hover .menuEllipsis,
.storedChildMenu:focus .menuEllipsis {
display: block;
}
.contextmenu6 {
top: -29px;
}
.userDefinedMenu:hover .contextmenu6 {
display: block;
}
.userDefinedchildMenu:hover .menuEllipsis,
.userDefinedchildMenu:focus .menuEllipsis {
display: block;
}
.triggersMenu:hover .menuEllipsis,
.triggersMenu:focus .menuEllipsis {
display: block;
}
.triggersChildMenu:hover .menuEllipsis,
.triggersChildMenu:focus .menuEllipsis {
display: block;
}
.databaseId {
font-size: 14px;
}
.storedUdfTriggerMenu {
padding-left: 0px;
}
.collectionstree img {
width: 16px;
height: 16px;
vertical-align: text-top;
}
img.collectionsTreeCollapseExpand {
width: 10px;
height: 10px;
vertical-align: middle;
margin-bottom: 5px;
}
.collapsed::before {
content: "\23F5";
margin-left: 0px;
font-size: 15px;
}
.expanded::before {
content: "\23F7";
margin-left: 0px;
font-size: 15px;
}
.collectionMenuChildren {
padding-left: 42px;
}
.main-nav {
width: 100vh;
height: 40px;
background: white;
transform-origin: left top;
-webkit-transform-origin: left top;
-ms-transform-origin: left top;
transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%);
border-bottom: 1px solid #ccc;
}
.main-nav-img {
width: 16px;
height: 16px;
margin: -32px 0 0 0;
transform: rotate(-90deg) translateX(-100%);
-webkit-transform: rotate(-90deg) translateX(-100%);
-ms-transform: rotate(-90deg) translateX(-100%);
}
.main-nav-img.main-nav-sub-img {
width: 16px;
height: 16px;
margin: 0px 0px 0 0;
transform: rotate(180deg) translateX(0%);
-webkit-transform: rotate(180deg) translateX(0%);
-ms-transform: rotate(180deg) translateX(0%);
position: absolute;
right: -8px;
top: 16px;
}
ul.nav {
margin: 0 auto;
margin-top: 0px;
margin-left: 0px;
}
.mini ul.nav li {
float: right;
line-height: 25px;
height: auto;
margin-top: 3px;
}
.spancolchildstyle {
padding: 4px;
}
.contextmenubutton {
float: right;
display: none;
}
.highlight:hover > .contextmenubutton {
display: unset;
}
.highlight:hover > .contextmenubutton::after {
content: "\2026";
font-size: 12px;
}
.showEllipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}

359
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.7.0",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
@@ -116,8 +116,6 @@
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
"zustand": "3.5.0"
},
"devDependencies": {
@@ -125,7 +123,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.57.0",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -292,71 +290,59 @@
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@azure/core-http-compat": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@azure/core-http-compat/-/core-http-compat-2.3.0.tgz",
"integrity": "sha512-qLQujmUypBBG0gxHd0j6/Jdmul6ttl24c8WGiLXIk7IHXdBlfoBqW27hyz3Xn6xbfdyVSarl1Ttbk0AwnZBYCw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-client": "^1.3.0",
"@azure/core-rest-pipeline": "^1.20.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-lro": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.2.tgz",
"integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.2.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-lro/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/core-paging": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz",
"integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==",
"dependencies": {
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-paging/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz",
"integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==",
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.18.0.tgz",
"integrity": "sha512-QSoGUp4Eq/gohEFNJaUOwTN7BCc2nHTjjbm75JT0aD7W65PWM1H/tItz0GsABn22uaKyGxiMhWQLt2r+FGU89Q==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"@typespec/ts-http-runtime": "^0.2.2",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/agent-base": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
"integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
"dependencies": {
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/https-proxy-agent": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
"integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@azure/core-rest-pipeline/node_modules/tslib": {
"version": "2.6.2",
"license": "0BSD"
@@ -393,25 +379,23 @@
"license": "0BSD"
},
"node_modules/@azure/cosmos": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.7.0.tgz",
"integrity": "sha512-a8OV7E41u/ZDaaaDAFdqTTiJ7c82jZc/+ot3XzNCIIilR25NBB+1ixzWQOAgP8SHRUIKfaUl6wAPdTuiG9I66A==",
"license": "MIT",
"version": "4.2.0-beta.1",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.2.0-beta.1.tgz",
"integrity": "sha512-mREONehm1DxjEKXGaNU6Wmpf9Ckb9IrhKFXhDFVs45pxmoEb3y2s/Ub0owuFmqlphpcS1zgtYQn5exn+lwnJuQ==",
"dependencies": {
"@azure/abort-controller": "^2.1.2",
"@azure/core-auth": "^1.9.0",
"@azure/core-rest-pipeline": "^1.19.1",
"@azure/core-tracing": "^1.2.0",
"@azure/core-util": "^1.11.0",
"@azure/keyvault-keys": "^4.9.0",
"@azure/logger": "^1.1.4",
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.7.1",
"@azure/core-rest-pipeline": "^1.15.1",
"@azure/core-tracing": "^1.1.1",
"@azure/core-util": "^1.8.1",
"fast-json-stable-stringify": "^2.1.0",
"jsbi": "^4.3.0",
"priorityqueuejs": "^2.0.0",
"semaphore": "^1.1.0",
"tslib": "^2.8.1"
"tslib": "^2.6.2"
},
"engines": {
"node": ">=20.0.0"
"node": ">=18.0.0"
}
},
"node_modules/@azure/cosmos-language-service": {
@@ -441,9 +425,8 @@
}
},
"node_modules/@azure/cosmos/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
"version": "2.6.2",
"license": "0BSD"
},
"node_modules/@azure/identity": {
"version": "4.5.0",
@@ -509,66 +492,14 @@
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/keyvault-common": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-common/-/keyvault-common-2.0.0.tgz",
"integrity": "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.10.0",
"@azure/logger": "^1.1.4",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-common/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/keyvault-keys": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@azure/keyvault-keys/-/keyvault-keys-4.9.0.tgz",
"integrity": "sha512-ZBP07+K4Pj3kS4TF4XdkqFcspWwBHry3vJSOFM5k5ZABvf7JfiMonvaFk2nBF6xjlEbMpz5PE1g45iTMme0raQ==",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.3.0",
"@azure/core-client": "^1.5.0",
"@azure/core-http-compat": "^2.0.1",
"@azure/core-lro": "^2.2.0",
"@azure/core-paging": "^1.1.1",
"@azure/core-rest-pipeline": "^1.8.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
"@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@azure/keyvault-keys/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@azure/logger": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz",
"integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==",
"version": "1.0.4",
"license": "MIT",
"dependencies": {
"@typespec/ts-http-runtime": "^0.2.2",
"tslib": "^2.6.2"
"tslib": "^2.2.0"
},
"engines": {
"node": ">=18.0.0"
"node": ">=14.0.0"
}
},
"node_modules/@azure/logger/node_modules/tslib": {
@@ -628,14 +559,6 @@
}
}
},
"node_modules/@azure/ms-rest-js/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@azure/ms-rest-js/node_modules/xml2js": {
"version": "0.5.0",
"license": "MIT",
@@ -695,14 +618,6 @@
"node": ">=0.8.0"
}
},
"node_modules/@azure/msal-node/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@babel/code-frame": {
"version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
@@ -7613,14 +7528,6 @@
"uuid": "^8.0.0"
}
},
"node_modules/@nteract/commutable/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nteract/connected-components": {
"version": "6.8.2",
"license": "BSD-3-Clause",
@@ -9151,14 +9058,6 @@
"uuid": "^8.0.0"
}
},
"node_modules/@nteract/fixtures/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nteract/iron-icons": {
"version": "1.0.0",
"license": "BSD-3-Clause",
@@ -9316,14 +9215,6 @@
"uuid": "^8.0.0"
}
},
"node_modules/@nteract/messaging/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nteract/monaco-editor": {
"version": "3.2.2",
"license": "BSD-3-Clause",
@@ -9439,14 +9330,6 @@
"version": "0.18.1",
"license": "MIT"
},
"node_modules/@nteract/monaco-editor/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nteract/mythic-configuration": {
"version": "1.0.12",
"license": "BSD-3-Clause",
@@ -9715,14 +9598,6 @@
"uuid": "^8.0.0"
}
},
"node_modules/@nteract/reducers/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@nteract/selectors": {
"version": "3.2.0",
"license": "BSD-3-Clause",
@@ -9946,14 +9821,6 @@
"uuid": "^8.0.0"
}
},
"node_modules/@nteract/types/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@octokit/auth-token": {
"version": "4.0.0",
"license": "MIT",
@@ -10321,13 +10188,12 @@
}
},
"node_modules/@playwright/test": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
"integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz",
"integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.57.0"
"playwright": "1.49.1"
},
"bin": {
"playwright": "cli.js"
@@ -13208,56 +13074,6 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typespec/ts-http-runtime": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz",
"integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==",
"dependencies": {
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/agent-base": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
"integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@typespec/ts-http-runtime/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/@ungap/url-search-params": {
"version": "0.2.2",
"license": "ISC"
@@ -26486,15 +26302,6 @@
"xmlbuilder": "^15.1.0"
}
},
"node_modules/jest-trx-results-processor/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/jest-util": {
"version": "24.9.0",
"license": "MIT",
@@ -27256,6 +27063,11 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbi": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
},
"node_modules/jsbn": {
"version": "0.1.1",
"license": "MIT"
@@ -30904,13 +30716,12 @@
}
},
"node_modules/playwright": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
"integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz",
"integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.57.0"
"playwright-core": "1.49.1"
},
"bin": {
"playwright": "cli.js"
@@ -30923,11 +30734,10 @@
}
},
"node_modules/playwright-core": {
"version": "1.57.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
"integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
"version": "1.49.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz",
"integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
@@ -33831,15 +33641,6 @@
"websocket-driver": "^0.7.4"
}
},
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"license": "BSD-3-Clause",
@@ -35706,9 +35507,8 @@
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"version": "8.3.2",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
@@ -35934,11 +35734,6 @@
"defaults": "^1.0.3"
}
},
"node_modules/web-vitals": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz",
"integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"license": "BSD-2-Clause"

View File

@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"@azure/arm-cosmosdb": "9.1.0",
"@azure/cosmos": "4.7.0",
"@azure/cosmos": "4.2.0-beta.1",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "4.5.0",
"@azure/msal-browser": "2.14.2",
@@ -46,8 +46,8 @@
"@types/mkdirp": "1.0.1",
"@types/node-fetch": "2.5.7",
"@xmldom/xmldom": "0.7.13",
"@xterm/addon-fit": "0.10.0",
"@xterm/xterm": "5.5.0",
"@xterm/addon-fit": "0.10.0",
"allotment": "1.20.2",
"applicationinsights": "1.8.0",
"bootstrap": "3.4.1",
@@ -111,8 +111,6 @@
"tinykeys": "2.1.0",
"underscore": "1.12.1",
"utility-types": "3.10.0",
"uuid": "9.0.0",
"web-vitals": "4.2.4",
"zustand": "3.5.0"
},
"devDependencies": {
@@ -120,7 +118,7 @@
"@babel/preset-env": "7.24.7",
"@babel/preset-react": "7.24.7",
"@babel/preset-typescript": "7.24.7",
"@playwright/test": "1.57.0",
"@playwright/test": "1.49.1",
"@testing-library/react": "11.2.3",
"@types/applicationinsights-js": "1.0.7",
"@types/codemirror": "0.0.56",
@@ -213,11 +211,7 @@
"copyToConsumers": "node copyToConsumers",
"test": "rimraf coverage && jest",
"test:debug": "jest --runInBand",
"test:e2e": "npx playwright test",
"test:e2e:setup:verify": "npx playwright test test/setup-verification.spec.ts",
"test:e2e:setup": "node setup-tests.js",
"test:e2e:headed": "npx playwright test --headed",
"test:e2e:ui": "npx playwright test --ui",
"test:e2e": "jest -c ./jest.config.playwright.js --detectOpenHandles",
"test:file": "jest --coverage=false",
"watch": "npm run start",
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",

View File

@@ -6,17 +6,15 @@ export default defineConfig({
testDir: "test",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 3 : 2,
retries: process.env.CI ? 3 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? "blob" : "html",
timeout: 10 * 60 * 1000,
globalSetup: require.resolve('./test/global-setup.ts'),
use: {
trace: "retain-on-failure",
video: "retain-on-failure",
trace: "off",
video: "off",
screenshot: "on",
testIdAttribute: "data-test",
// storageState: './test/../playwright/.auth/user.json',
contextOptions: {
ignoreHTTPSErrors: true,
},

View File

@@ -1,6 +1,6 @@
[defaults]
group = dataexplorer-preview
sku = P1v2
sku = P1V2
appserviceplan = dataexplorer-preview
location = westus2
web = dataexplorer-preview

View File

@@ -7,6 +7,7 @@ const backendEndpoint = "https://cdb-ms-mpac-pbe.cosmos.azure.com";
const previewSiteEndpoint = "https://dataexplorer-preview.azurewebsites.net";
const previewStorageWebsiteEndpoint = "https://dataexplorerpreview.z5.web.core.windows.net/";
const githubApiUrl = "https://api.github.com/repos/Azure/cosmos-explorer";
const githubPullRequestUrl = "https://github.com/Azure/cosmos-explorer/pull";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
const api = createProxyMiddleware({
@@ -56,7 +57,11 @@ app.get("/pull/:pr(\\d+)", (req, res) => {
fetch(`${githubApiUrl}/pulls/${pr}`)
.then((response) => response.json())
.then(({ head: { sha } }) => {
.then(({ head: { ref, sha } }) => {
const prUrl = new URL(`${githubPullRequestUrl}/${pr}`);
prUrl.hash = ref;
search.set("feature.pr", prUrl.href);
const explorer = new URL(`${previewSiteEndpoint}/commit/${sha}/explorer.html`);
explorer.search = search.toString();

36281
preview/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,16 @@
"description": "",
"main": "index.js",
"scripts": {
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:20-lts\" --sku P1V2",
"deploy": "az webapp up --name \"dataexplorer-preview\" --subscription \"cosmosdb-portalteam-runners\" --resource-group \"dataexplorer-preview\" --runtime \"NODE:18-lts\" --sku P1V2",
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Microsoft Corporation",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.2",
"express": "^4.17.1",
"http-proxy-middleware": "^3.0.3",
"node": "^20.19.5",
"node-fetch": "^2.6.1",
"path-to-regexp": "^0.1.12"
"node": "^18.20.6",
"node-fetch": "^2.6.1"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
/**
* Manual setup script to run the pre-flight Azure authentication and configuration
* This can be used independently of Playwright for setting up the testing environment
*/
const { exec } = require('child_process');
const { promisify } = require('util');
const path = require('path');
const execAsync = promisify(exec);
async function runManualSetup() {
console.log('🚀 Starting manual Azure Cosmos DB Explorer test setup...');
try {
// Import the global setup function and run it
const globalSetup = require('./test/global-setup.js');
// Create a minimal config object that matches what Playwright would pass
const mockConfig = {
configFile: path.join(__dirname, 'playwright.config.ts'),
rootDir: __dirname,
testDir: path.join(__dirname, 'test'),
projects: []
};
await globalSetup(mockConfig);
console.log('✅ Manual setup completed successfully!');
console.log('\nYou can now run your Playwright tests with:');
console.log(' npm run test:e2e');
console.log(' or');
console.log(' npx playwright test');
} catch (error) {
console.error('❌ Manual setup failed:', error.message);
process.exit(1);
}
}
// Only run if this script is executed directly
if (require.main === module) {
runManualSetup();
}
module.exports = { runManualSetup };

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env node
/**
* Manual setup script to run the pre-flight Azure authentication and configuration
* This can be used independently of Playwright for setting up the testing environment
*/
async function runManualSetup(): Promise<void> {
console.log('🚀 Starting manual Azure Cosmos DB Explorer test setup...');
try {
// Dynamically import the Playwright global setup
const globalSetupModule = await import('./test/global-setup');
const globalSetup = globalSetupModule.default;
if (typeof globalSetup !== 'function') {
throw new Error('global-setup.ts does not export a default function');
}
// Minimal mock config similar to what Playwright provides
/* const mockConfig: FullConfig = {
configFile: path.join(__dirname, 'playwright.config.ts'),
rootDir: __dirname,
testDir: path.join(__dirname, 'test'),
projects: []
}; */
await globalSetup();
console.log('✅ Manual setup completed successfully!');
console.log('\nYou can now run your Playwright tests with:');
console.log(' npm run test:e2e');
console.log(' or');
console.log(' npx playwright test');
} catch (error: any) {
console.error('❌ Manual setup failed:', error.message);
process.exit(1);
}
}
// Only run if executed directly (not imported)
if (require.main === module) {
void runManualSetup();
}
export { runManualSetup };

View File

@@ -90,10 +90,6 @@ export class CapabilityNames {
public static readonly EnableServerless: string = "EnableServerless";
public static readonly EnableNoSQLVectorSearch: string = "EnableNoSQLVectorSearch";
public static readonly EnableNoSQLFullTextSearch: string = "EnableNoSQLFullTextSearch";
public static readonly EnableDataMasking: string = "EnableDataMasking";
public static readonly EnableDynamicDataMasking: string = "EnableDynamicDataMasking";
public static readonly EnableNoSQLFullTextSearchPreviewFeatures: string = "EnableNoSQLFullTextSearchPreviewFeatures";
public static readonly EnableOnlineCopyFeature: string = "EnableOnlineContainerCopy";
}
export enum CapacityMode {
@@ -142,12 +138,13 @@ export enum MongoBackendEndpointType {
remote,
}
export class AadScopeEndpoints {
public static readonly Development: string = "https://cosmos.azure.com";
public static readonly MPAC: string = "https://cosmos.azure.com";
public static readonly Prod: string = "https://cosmos.azure.com";
public static readonly Fairfax: string = "https://cosmos.azure.us";
public static readonly Mooncake: string = "https://cosmos.azure.cn";
export class BackendApi {
public static readonly GenerateToken: string = "GenerateToken";
public static readonly PortalSettings: string = "PortalSettings";
public static readonly AccountRestrictions: string = "AccountRestrictions";
public static readonly RuntimeProxy: string = "RuntimeProxy";
public static readonly DisallowedLocations: string = "DisallowedLocations";
public static readonly SampleData: string = "SampleData";
}
export class PortalBackendEndpoints {
@@ -267,7 +264,6 @@ export class HttpHeaders {
public static activityId: string = "x-ms-activity-id";
public static apiType: string = "x-ms-cosmos-apitype";
public static authorization: string = "authorization";
public static entraIdToken: string = "x-ms-entraid-token";
public static collectionIndexTransformationProgress: string =
"x-ms-documentdb-collection-index-transformation-progress";
public static continuation: string = "x-ms-continuation";
@@ -297,7 +293,6 @@ export class HttpHeaders {
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
public static xAPIKey: string = "X-API-Key";
public static sessionId: string = "x-ms-client-session-id";
}
export class ContentType {
@@ -779,10 +774,3 @@ export const ShortenedQueryCopilotSampleContainerSchema = {
userPrompt: "find all products",
};
export enum MongoGuidRepresentation {
Standard = "Standard",
CSharpLegacy = "CSharpLegacy",
JavaLegacy = "JavaLegacy",
PythonLegacy = "PythonLegacy",
}

View File

@@ -4,12 +4,12 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { AuthorizationToken } from "Contracts/FabricMessageTypes";
import { checkDatabaseResourceTokensValidity, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { useDataplaneRbacAuthorization } from "Utils/AuthorizationUtils";
import { AuthType } from "../AuthType";
import { PriorityLevel } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { Platform, configContext } from "../ConfigContext";
import { FabricArtifactInfo, updateUserContext, userContext } from "../UserContext";
import { isDataplaneRbacSupported } from "../Utils/APITypeUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import * as PriorityBasedExecutionUtils from "../Utils/PriorityBasedExecutionUtils";
import { EmulatorMasterKey, HttpHeaders } from "./Constants";
@@ -20,7 +20,8 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: Cosmos.RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo;
if (useDataplaneRbacAuthorization(userContext)) {
const dataPlaneRBACOptionEnabled = userContext.dataPlaneRbacEnabled && isDataplaneRbacSupported(userContext.apiType);
if (userContext.features.enableAadDataPlane || dataPlaneRBACOptionEnabled) {
Logger.logInfo(
`AAD Data Plane Feature flag set to ${userContext.features.enableAadDataPlane} for account with disable local auth ${userContext.databaseAccount.properties.disableLocalAuth} `,
"Explorer/tokenProvider",

View File

@@ -1,7 +1,7 @@
import { TagNames, WorkloadType } from "Common/Constants";
import { Tags } from "Contracts/DataModels";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { ApiType, userContext } from "../UserContext";
import { userContext } from "../UserContext";
function isVirtualNetworkFilterEnabled() {
return userContext.databaseAccount?.properties?.isVirtualNetworkFilterEnabled;
@@ -33,33 +33,3 @@ export function isGlobalSecondaryIndexEnabled(): boolean {
!isFabric() && userContext.apiType === "SQL" && userContext.databaseAccount?.properties?.enableMaterializedViews
);
}
export const getDatabaseEndpoint = (apiType: ApiType): string => {
switch (apiType) {
case "Mongo":
return "mongodbDatabases";
case "Cassandra":
return "cassandraKeyspaces";
case "Gremlin":
return "gremlinDatabases";
case "Tables":
return "tables";
case "SQL":
default:
return "sqlDatabases";
}
};
export const getCollectionEndpoint = (apiType: ApiType): string => {
switch (apiType) {
case "Mongo":
return "collections";
case "Cassandra":
return "tables";
case "Gremlin":
return "graphs";
case "SQL":
default:
return "containers";
}
};

View File

@@ -28,39 +28,3 @@ describe("Environment Utility Test", () => {
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Development);
});
});
describe("normalizeArmEndpoint", () => {
it("should append '/' if not present", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com")).toBe("https://example.com/");
});
it("should return the same uri if '/' is present at the end", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("https://example.com/")).toBe("https://example.com/");
});
it("should handle empty string", () => {
expect(EnvironmentUtility.normalizeArmEndpoint("")).toBe("");
});
});
describe("getEnvironment", () => {
it("should return Prod environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Prod);
});
it("should return Fairfax environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Fairfax,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Fairfax);
});
it("should return Mooncake environment", () => {
updateConfigContext({
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Mooncake,
});
expect(EnvironmentUtility.getEnvironment()).toBe(EnvironmentUtility.Environment.Mooncake);
});
});

View File

@@ -1,5 +1,4 @@
import { AadScopeEndpoints, PortalBackendEndpoints } from "Common/Constants";
import * as Logger from "Common/Logger";
import { PortalBackendEndpoints } from "Common/Constants";
import { configContext } from "ConfigContext";
export function normalizeArmEndpoint(uri: string): string {
@@ -28,17 +27,3 @@ export const getEnvironment = (): Environment => {
return environmentMap[configContext.PORTAL_BACKEND_ENDPOINT];
};
export const getEnvironmentScopeEndpoint = (): string => {
const environment = getEnvironment();
const endpoint = AadScopeEndpoints[environment];
if (!endpoint) {
throw new Error("Cannot determine AAD scope endpoint");
}
const hrefEndpoint = new URL(endpoint).href.replace(/\/+$/, "/.default");
Logger.logInfo(
`Using AAD scope endpoint: ${hrefEndpoint}, Environment: ${environment}`,
"EnvironmentUtility/getEnvironmentScopeEndpoint",
);
return hrefEndpoint;
};

View File

@@ -23,10 +23,7 @@ export const handleError = (error: string | ARMError | Error, area: string, cons
};
export const getErrorMessage = (error: string | Error = ""): string => {
let errorMessage = typeof error === "string" ? error : error.message;
if (!errorMessage) {
errorMessage = JSON.stringify(error);
}
const errorMessage = typeof error === "string" ? error : error.message;
return replaceKnownError(errorMessage);
};

View File

@@ -1,3 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import * as Constants from "../Common/Constants";
import { QueryResults } from "../Contracts/ViewModels";
@@ -13,14 +14,18 @@ interface QueryResponse {
}
export interface MinimalQueryIterator {
fetchNext: () => Promise<QueryResponse>;
fetchNext: (queryOperationOptions?: QueryOperationOptions) => Promise<QueryResponse>;
}
// Pick<QueryIterator<any>, "fetchNext">;
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
export function nextPage(
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> {
TelemetryProcessor.traceStart(Action.ExecuteQuery);
return documentsIterator.fetchNext().then((response) => {
return documentsIterator.fetchNext(queryOperationOptions).then((response) => {
TelemetryProcessor.traceSuccess(Action.ExecuteQuery, { dataExplorerArea: Constants.Areas.Tab });
const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -1,52 +0,0 @@
import { render } from "@testing-library/react";
import React from "react";
import LoadingOverlay from "./LoadingOverlay";
describe("LoadingOverlay", () => {
const defaultProps = {
isLoading: true,
label: "Loading...",
};
it("should render loading overlay when isLoading is true", () => {
const { container } = render(<LoadingOverlay {...defaultProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("should render loading overlay with custom label", () => {
const customProps = {
isLoading: true,
label: "Processing your request...",
};
const { container } = render(<LoadingOverlay {...customProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("should render loading overlay with empty label", () => {
const emptyLabelProps = {
isLoading: true,
label: "",
};
const { container } = render(<LoadingOverlay {...emptyLabelProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it("should return null when isLoading is false", () => {
const notLoadingProps = {
isLoading: false,
label: "Loading...",
};
const { container } = render(<LoadingOverlay {...notLoadingProps} />);
expect(container.firstChild).toBeNull();
});
it("should handle long labels properly", () => {
const longLabelProps = {
isLoading: true,
label:
"This is a very long loading message that might span multiple lines and should still render correctly in the loading overlay component",
};
const { container } = render(<LoadingOverlay {...longLabelProps} />);
expect(container.firstChild).toMatchSnapshot();
});
});

View File

@@ -1,38 +0,0 @@
import { Overlay, Spinner, SpinnerSize } from "@fluentui/react";
import { useThemeStore } from "hooks/useTheme";
import React from "react";
interface LoadingOverlayProps {
isLoading: boolean;
label: string;
}
const LoadingOverlay: React.FC<LoadingOverlayProps> = ({ isLoading, label }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
if (!isLoading) {
return null;
}
return (
<Overlay
data-test="loading-overlay"
styles={{
root: {
backgroundColor: isDarkMode ? "rgba(32, 31, 30, 0.9)" : "rgba(255,255,255,0.9)",
zIndex: 9999,
display: "flex",
alignItems: "center",
justifyContent: "center",
},
}}
>
<Spinner
size={SpinnerSize.large}
label={label}
styles={{ label: { fontWeight: 600, color: isDarkMode ? "#ffffff" : "#323130" } }}
/>
</Overlay>
);
};
export default LoadingOverlay;

View File

@@ -65,6 +65,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -83,6 +84,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
queryDocuments(databaseId, collection, true, "{}");
expect(window.fetch).toHaveBeenCalledWith(
@@ -99,6 +101,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -117,6 +120,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -133,6 +137,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -151,6 +156,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
readDocument(databaseId, collection, documentId);
expect(window.fetch).toHaveBeenCalledWith(
@@ -167,6 +173,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -190,6 +197,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
window.fetch = jest.fn().mockImplementation(fetchMock);
});
@@ -208,6 +216,7 @@ describe("MongoProxyClient", () => {
it("builds the correct proxy URL in development", () => {
updateConfigContext({
MONGO_PROXY_ENDPOINT: "https://localhost:1234",
globallyEnabledMongoAPIs: [],
});
deleteDocuments(databaseId, collection, [documentId]);
expect(window.fetch).toHaveBeenCalledWith(
@@ -224,6 +233,7 @@ describe("MongoProxyClient", () => {
});
updateConfigContext({
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
globallyEnabledMongoAPIs: [],
});
});

View File

@@ -1,5 +1,4 @@
import { Constants as CosmosSDKConstants } from "@azure/cosmos";
import { getMongoGuidRepresentation } from "Shared/StorageUtility";
import { AuthType } from "../AuthType";
import { configContext } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
@@ -7,7 +6,6 @@ import { MessageTypes } from "../Contracts/ExplorerContracts";
import { Collection } from "../Contracts/ViewModels";
import DocumentId from "../Explorer/Tree/DocumentId";
import { userContext } from "../UserContext";
import { isDataplaneRbacEnabledForProxyApi } from "../Utils/AuthorizationUtils";
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
import { ApiType, ContentType, HttpHeaders, HttpStatusCodes } from "./Constants";
import { MinimalQueryIterator } from "./IteratorUtilities";
@@ -17,20 +15,13 @@ const defaultHeaders = {
[HttpHeaders.apiType]: ApiType.MongoDB.toString(),
[CosmosSDKConstants.HttpHeaders.MaxEntityCount]: "100",
[CosmosSDKConstants.HttpHeaders.Version]: "2017-11-15",
[HttpHeaders.sessionId]: userContext.sessionId,
};
function authHeaders() {
if (userContext.authType === AuthType.EncryptedToken) {
return { [HttpHeaders.guestAccessToken]: userContext.accessToken };
} else {
const headers: { [key: string]: string } = {
[HttpHeaders.authorization]: userContext.authorizationToken,
};
if (isDataplaneRbacEnabledForProxyApi(userContext)) {
headers[HttpHeaders.entraIdToken] = userContext.aadToken;
}
return headers;
return { [HttpHeaders.authorization]: userContext.authorizationToken };
}
}
@@ -148,9 +139,6 @@ export function readDocument(
documentId && documentId.partitionKey && !documentId.partitionKey.systemKey
? documentId.partitionKeyProperties?.[0]
: "",
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -193,9 +181,6 @@ export function createDocument(
partitionKey:
collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
documentContent: JSON.stringify(documentContent),
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -243,9 +228,6 @@ export function updateDocument(
? documentId.partitionKeyProperties?.[0]
: "",
documentContent,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);
@@ -292,9 +274,6 @@ export function deleteDocuments(
subscriptionID: userContext.subscriptionId,
resourceGroup: userContext.resourceGroup,
databaseAccountName: databaseAccount.name,
clientSettings: {
guidRepresentation: getMongoGuidRepresentation(),
},
};
const endpoint = getEndpoint(configContext.MONGO_PROXY_ENDPOINT);

View File

@@ -1,24 +0,0 @@
.pager-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
gap: 16px;
}
.pager-container > div {
display: flex;
gap: 8px;
align-items: center;
}
/* Override dark mode inherit for pagination icons */
body.isDarkMode .pager-container .ms-Button .ms-Button-icon,
body.isDarkMode .pager-container .ms-Button i {
color: var(--colorBrandForeground1);
}
body.isDarkMode .pager-container .ms-Button:disabled .ms-Button-icon,
body.isDarkMode .pager-container .ms-Button:disabled i {
color: var(--colorNeutralForegroundDisabled);
}

View File

@@ -1,111 +0,0 @@
import { IconButton, Text } from "@fluentui/react";
import * as React from "react";
import "./Pager.css";
export interface PagerProps {
startIndex: number;
totalCount: number;
pageSize: number;
onLoadPage: (startIndex: number, pageSize: number) => void;
disabled?: boolean;
showFirstLast?: boolean;
showItemCount?: boolean;
className?: string;
}
const iconButtonStyles = {
root: {
backgroundColor: "transparent",
},
rootHovered: {
backgroundColor: "transparent",
},
rootPressed: {
backgroundColor: "transparent",
},
rootDisabled: {
backgroundColor: "transparent",
},
rootFocused: {
backgroundColor: "transparent",
outline: "none",
},
};
const Pager: React.FC<PagerProps> = ({
startIndex,
totalCount,
pageSize,
onLoadPage,
disabled = false,
showFirstLast = true,
showItemCount = true,
className,
}) => {
// Calculate current page and total pages from startIndex
const currentPage = Math.floor(startIndex / pageSize) + 1;
const totalPages = Math.ceil(totalCount / pageSize);
const endIndex = Math.min(startIndex + pageSize, totalCount);
const handleFirstPage = () => onLoadPage(0, pageSize);
const handlePreviousPage = () => onLoadPage(startIndex - pageSize, pageSize);
const handleNextPage = () => onLoadPage(startIndex + pageSize, pageSize);
const handleLastPage = () => onLoadPage((totalPages - 1) * pageSize, pageSize);
if (totalCount === 0) {
return null;
}
return (
<div className={className || "pager-container"}>
{showItemCount && (
<Text className="themeText">
Showing {startIndex + 1} - {endIndex} of {totalCount} items
</Text>
)}
<div>
{showFirstLast && (
<IconButton
iconProps={{ iconName: "DoubleChevronLeft" }}
title="First page"
ariaLabel="Go to first page"
onClick={handleFirstPage}
disabled={disabled || currentPage === 1}
styles={iconButtonStyles}
/>
)}
<IconButton
iconProps={{ iconName: "ChevronLeft" }}
title="Previous page"
ariaLabel="Go to previous page"
onClick={handlePreviousPage}
disabled={disabled || currentPage === 1}
styles={iconButtonStyles}
/>
<Text className="themeText">
Page {currentPage} of {totalPages}
</Text>
<IconButton
iconProps={{ iconName: "ChevronRight" }}
title="Next page"
ariaLabel="Go to next page"
onClick={handleNextPage}
disabled={disabled || currentPage === totalPages}
styles={iconButtonStyles}
/>
{showFirstLast && (
<IconButton
iconProps={{ iconName: "DoubleChevronRight" }}
title="Last page"
ariaLabel="Go to last page"
onClick={handleLastPage}
disabled={disabled || currentPage === totalPages}
styles={iconButtonStyles}
/>
)}
</div>
</div>
);
};
export default Pager;

View File

@@ -1,4 +1,5 @@
import { monaco } from "Explorer/LazyMonaco";
import { getRUThreshold, ruThresholdEnabled } from "Shared/StorageUtility";
export enum QueryErrorSeverity {
Error = "Error",
@@ -102,9 +103,20 @@ export interface ErrorEnrichment {
learnMoreUrl?: string;
}
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {};
const REPLACEMENT_MESSAGES: Record<string, (original: string) => string> = {
OPERATION_RU_LIMIT_EXCEEDED: (original) => {
if (ruThresholdEnabled()) {
const threshold = getRUThreshold();
return `Query exceeded the Request Unit (RU) limit of ${threshold} RUs. You can change this limit in Data Explorer settings.`;
}
return original;
},
};
const HELP_LINKS: Record<string, string> = {};
const HELP_LINKS: Record<string, string> = {
OPERATION_RU_LIMIT_EXCEEDED:
"https://learn.microsoft.com/en-us/azure/cosmos-db/data-explorer#configure-request-unit-threshold",
};
export default class QueryError {
message: string;

View File

@@ -1,32 +0,0 @@
import { Shimmer, ShimmerElementType, Stack } from "@fluentui/react";
import * as React from "react";
export interface IndentLevel {
level: number;
width?: string;
}
interface ShimmerTreeProps {
indentLevels: IndentLevel[];
style?: React.CSSProperties;
}
const ShimmerTree = ({ indentLevels, style = {} }: ShimmerTreeProps) => {
const renderShimmers = (indent: IndentLevel) => (
<Shimmer
key={Math.random()}
shimmerElements={[
{ type: ShimmerElementType.gap, width: `${indent.level * 20}px` },
{ type: ShimmerElementType.line, height: 16, width: indent.width || "100%" },
]}
style={{ marginBottom: 8 }}
/>
);
return (
<Stack tokens={{ childrenGap: 8 }} style={{ width: "50%", ...style }} data-testid="shimmer-stack">
{indentLevels.map((indentLevel: IndentLevel) => renderShimmers(indentLevel))}
</Stack>
);
};
export default ShimmerTree;

View File

@@ -50,33 +50,10 @@ export const Upload: FunctionComponent<UploadProps> = ({
const title = label + " to upload";
return (
<div>
<span className="renewUploadItemsHeader" style={{ color: "var(--colorNeutralForeground1)" }}>
{label}
</span>
<span className="renewUploadItemsHeader">{label}</span>
{tooltip && <InfoTooltip>{tooltip}</InfoTooltip>}
<Stack horizontal>
<TextField
styles={{
fieldGroup: {
width: 300,
backgroundColor: "var(--colorNeutralBackground3)",
borderColor: "var(--colorNeutralStroke1)",
},
field: {
backgroundColor: "var(--colorNeutralBackground3)",
color: "var(--colorNeutralForeground1)",
},
subComponentStyles: {
label: {
root: {
color: "var(--colorNeutralForeground1)",
},
},
},
}}
readOnly
value={selectedFilesTitle.toString()}
/>
<TextField styles={{ fieldGroup: { width: 300 } }} readOnly value={selectedFilesTitle.toString()} />
<input
type="file"
id="importFileInput"

View File

@@ -1,77 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LoadingOverlay should handle long labels properly 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-112"
/>
<div
class="ms-Spinner-label label-113"
>
This is a very long loading message that might span multiple lines and should still render correctly in the loading overlay component
</div>
</div>
</div>
`;
exports[`LoadingOverlay should render loading overlay when isLoading is true 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-112"
/>
<div
class="ms-Spinner-label label-113"
>
Loading...
</div>
</div>
</div>
`;
exports[`LoadingOverlay should render loading overlay with custom label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-112"
/>
<div
class="ms-Spinner-label label-113"
>
Processing your request...
</div>
</div>
</div>
`;
exports[`LoadingOverlay should render loading overlay with empty label 1`] = `
<div
class="ms-Overlay root-109"
data-test="loading-overlay"
>
<div
class="ms-Spinner root-111"
>
<div
class="ms-Spinner-circle ms-Spinner--large circle-112"
/>
</div>
</div>
`;

View File

@@ -2,7 +2,7 @@
exports[`getCommonQueryOptions builds the correct default options objects 1`] = `
{
"enableQueryControl": false,
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 0,
@@ -13,7 +13,7 @@ exports[`getCommonQueryOptions builds the correct default options objects 1`] =
exports[`getCommonQueryOptions reads from localStorage 1`] = `
{
"enableQueryControl": false,
"disableNonStreamingOrderByQuery": true,
"enableScanInQuery": true,
"forceQueryPlan": true,
"maxDegreeOfParallelism": 17,

View File

@@ -1,6 +1,4 @@
import { ContainerRequest, ContainerResponse, DatabaseRequest, DatabaseResponse, RequestOptions } from "@azure/cosmos";
import { sendMessage } from "Common/MessageHandler";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
@@ -45,14 +43,6 @@ export const createCollection = async (params: DataModels.CreateCollectionParams
}
logConsoleInfo(`Successfully created container ${params.collectionId}`);
if (isFabricNative()) {
sendMessage({
type: FabricMessageTypes.ContainerUpdated,
params: { updateType: "created" },
});
}
return collection;
} catch (error) {
handleError(error, "CreateCollection", `Error while creating container ${params.collectionId}`);

View File

@@ -9,7 +9,7 @@ import {
SqlStoredProcedureCreateUpdateParameters,
SqlStoredProcedureResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -20,7 +20,6 @@ export async function createStoredProcedure(
): Promise<StoredProcedureDefinition & Resource> {
const clearMessage = logConsoleProgress(`Creating stored procedure ${storedProcedure.id}`);
try {
let resource: StoredProcedureDefinition & Resource;
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
@@ -61,16 +60,14 @@ export async function createStoredProcedure(
storedProcedure.id,
createSprocParams,
);
resource = rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
} else {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.create(storedProcedure);
resource = response.resource;
return rpResponse && (rpResponse.properties?.resource as StoredProcedureDefinition & Resource);
}
logConsoleInfo(`Successfully created stored procedure ${storedProcedure.id}`);
return resource;
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.storedProcedures.create(storedProcedure);
return response?.resource;
} catch (error) {
handleError(error, "CreateStoredProcedure", `Error while creating stored procedure ${storedProcedure.id}`);
throw error;

View File

@@ -3,7 +3,7 @@ import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
import { createUpdateSqlTrigger, getSqlTrigger } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
import { SqlTriggerCreateUpdateParameters, SqlTriggerResource } from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -14,7 +14,6 @@ export async function createTrigger(
): Promise<TriggerDefinition | SqlTriggerResource> {
const clearMessage = logConsoleProgress(`Creating trigger ${trigger.id}`);
try {
let resource: SqlTriggerResource | TriggerDefinition;
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
@@ -53,16 +52,14 @@ export async function createTrigger(
trigger.id,
createTriggerParams,
);
resource = rpResponse && rpResponse.properties?.resource;
} else {
const sdkResponse = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
resource = sdkResponse.resource;
return rpResponse && rpResponse.properties?.resource;
}
logConsoleInfo(`Successfully created trigger ${trigger.id}`);
return resource;
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.triggers.create(trigger as unknown as TriggerDefinition); // TODO: TypeScript does not like the SQL SDK trigger type
return response.resource;
} catch (error) {
handleError(error, "CreateTrigger", `Error while creating trigger ${trigger.id}`);
throw error;

View File

@@ -9,7 +9,7 @@ import {
SqlUserDefinedFunctionCreateUpdateParameters,
SqlUserDefinedFunctionResource,
} from "../../Utils/arm/generatedClients/cosmos/types";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { client } from "../CosmosClient";
import { handleError } from "../ErrorHandlingUtils";
@@ -20,7 +20,6 @@ export async function createUserDefinedFunction(
): Promise<UserDefinedFunctionDefinition & Resource> {
const clearMessage = logConsoleProgress(`Creating user defined function ${userDefinedFunction.id}`);
try {
let resource: UserDefinedFunctionDefinition & Resource;
if (
userContext.authType === AuthType.AAD &&
!userContext.features.enableSDKoperations &&
@@ -61,17 +60,14 @@ export async function createUserDefinedFunction(
userDefinedFunction.id,
createUDFParams,
);
resource = rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
} else {
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction);
resource = response.resource;
return rpResponse && (rpResponse.properties?.resource as UserDefinedFunctionDefinition & Resource);
}
logConsoleInfo(`Successfully created user defined function ${userDefinedFunction.id}`);
return resource;
const response = await client()
.database(databaseId)
.container(collectionId)
.scripts.userDefinedFunctions.create(userDefinedFunction);
return response?.resource;
} catch (error) {
handleError(
error,

View File

@@ -1,4 +1,3 @@
import { configContext } from "ConfigContext";
import { ApiType, userContext } from "UserContext";
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
import {
@@ -15,12 +14,9 @@ import {
DataTransferJobFeedResults,
DataTransferJobGetResults,
} from "Utils/arm/generatedClients/dataTransferService/types";
import { armRequest } from "Utils/arm/request";
import { addToPolling, removeFromPolling, updateDataTransferJob, useDataTransferJobs } from "hooks/useDataTransferJobs";
import promiseRetry, { AbortError, FailedAttemptError } from "p-retry";
export const DATA_TRANSFER_JOB_API_VERSION = "2025-05-01-preview";
export interface DataTransferParams {
jobName: string;
apiType: ApiType;
@@ -37,34 +33,26 @@ export const getDataTransferJobs = async (
subscriptionId: string,
resourceGroup: string,
accountName: string,
signal?: AbortSignal,
): Promise<DataTransferJobGetResults[]> => {
let dataTransferJobs: DataTransferJobGetResults[] = [];
let dataTransferFeeds: DataTransferJobFeedResults = await listByDatabaseAccount(
subscriptionId,
resourceGroup,
accountName,
signal,
);
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
while (dataTransferFeeds?.nextLink) {
/**
* The `nextLink` URL returned by the Cosmos DB SQL API pointed to an incorrect endpoint, causing timeouts.
* (i.e: https://cdbmgmtprodby.documents.azure.com:450/subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.DocumentDB/databaseAccounts/{account}/sql/dataTransferJobs?$top=100&$skiptoken=...)
* We manipulate the URL by parsing it to extract the path and query parameters,
* then construct the correct URL for the Azure Resource Manager (ARM) API.
* This ensures that the request is made to the correct base URL (`configContext.ARM_ENDPOINT`),
* which is required for ARM operations.
*/
const parsedUrl = new URL(dataTransferFeeds.nextLink);
const nextUrlPath = parsedUrl.pathname + parsedUrl.search;
dataTransferFeeds = await armRequest({
host: configContext.ARM_ENDPOINT,
path: nextUrlPath,
method: "GET",
apiVersion: DATA_TRANSFER_JOB_API_VERSION,
const nextResponse = await window.fetch(dataTransferFeeds.nextLink, {
headers: {
Authorization: userContext.authorizationToken,
},
});
dataTransferJobs.push(...(dataTransferFeeds?.value || []));
if (nextResponse.ok) {
dataTransferFeeds = await nextResponse.json();
dataTransferJobs = [...dataTransferJobs, ...(dataTransferFeeds?.value || [])];
} else {
break;
}
}
return dataTransferJobs;
};

View File

@@ -1,5 +1,3 @@
import { sendMessage } from "Common/MessageHandler";
import { FabricMessageTypes } from "Contracts/FabricMessageTypes";
import { isFabric } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { userContext } from "../../UserContext";
@@ -21,11 +19,6 @@ export async function deleteCollection(databaseId: string, collectionId: string)
await client().database(databaseId).container(collectionId).delete();
}
logConsoleInfo(`Successfully deleted container ${collectionId}`);
sendMessage({
type: FabricMessageTypes.ContainerUpdated,
params: { updateType: "deleted" },
});
} catch (error) {
handleError(error, "DeleteCollection", `Error while deleting container ${collectionId}`);
throw error;

View File

@@ -42,7 +42,6 @@ export interface IBulkDeleteResult {
export const deleteDocuments = async (
collection: CollectionBase,
documentIds: DocumentId[],
abortSignal: AbortSignal,
): Promise<IBulkDeleteResult[]> => {
const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`);
try {
@@ -66,16 +65,12 @@ export const deleteDocuments = async (
operationType: BulkOperationType.Delete,
}));
const promise = v2Container.items
.bulk(operations, undefined, {
abortSignal,
})
.then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
const promise = v2Container.items.bulk(operations).then((bulkResults) => {
return bulkResults.map((bulkResult, index) => {
const documentId = documentIdsChunk[index];
return { ...bulkResult, documentId };
});
});
promiseArray.push(promise);
}

View File

@@ -28,7 +28,6 @@ export async function deleteStoredProcedure(
} else {
await client().database(databaseId).container(collectionId).scripts.storedProcedure(storedProcedureId).delete();
}
logConsoleProgress(`Successfully deleted stored procedure ${storedProcedureId}`);
} catch (error) {
handleError(error, "DeleteStoredProcedure", `Error while deleting stored procedure ${storedProcedureId}`);
throw error;

View File

@@ -24,7 +24,6 @@ export async function deleteTrigger(databaseId: string, collectionId: string, tr
} else {
await client().database(databaseId).container(collectionId).scripts.trigger(triggerId).delete();
}
logConsoleProgress(`Successfully deleted trigger ${triggerId}`);
} catch (error) {
handleError(error, "DeleteTrigger", `Error while deleting trigger ${triggerId}`);
throw error;

View File

@@ -24,7 +24,6 @@ export async function deleteUserDefinedFunction(databaseId: string, collectionId
} else {
await client().database(databaseId).container(collectionId).scripts.userDefinedFunction(id).delete();
}
logConsoleProgress(`Successfully deleted user defined function ${id}`);
} catch (error) {
handleError(error, "DeleteUserDefinedFunction", `Error while deleting user defined function ${id}`);
throw error;

View File

@@ -1,4 +1,3 @@
import { isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import { configContext } from "../../ConfigContext";
import { userContext } from "../../UserContext";
@@ -42,7 +41,7 @@ interface MetricsResponse {
}
export const getCollectionUsageSizeInKB = async (databaseName: string, containerName: string): Promise<number> => {
if (userContext.authType !== AuthType.AAD || isFabricNative()) {
if (userContext.authType !== AuthType.AAD) {
return undefined;
}

View File

@@ -1,4 +1,5 @@
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
import { isVectorSearchEnabled } from "Utils/CapabilityUtils";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Queries } from "../Constants";
import { client } from "../CosmosClient";
@@ -25,7 +26,7 @@ export const getCommonQueryOptions = (options: FeedOptions): FeedOptions => {
options.maxItemCount ||
(storedItemPerPageSetting !== undefined && storedItemPerPageSetting) ||
Queries.itemsPerPage;
options.enableQueryControl = LocalStorageUtility.getEntryBoolean(StorageKey.QueryControlEnabled);
options.maxDegreeOfParallelism = LocalStorageUtility.getEntryNumber(StorageKey.MaxDegreeOfParellism);
options.disableNonStreamingOrderByQuery = !isVectorSearchEnabled();
return options;
};

View File

@@ -1,3 +1,4 @@
import { QueryOperationOptions } from "@azure/cosmos";
import { QueryResults } from "../../Contracts/ViewModels";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { getEntityName } from "../DocumentUtility";
@@ -8,12 +9,13 @@ export const queryDocumentsPage = async (
resourceName: string,
documentsIterator: MinimalQueryIterator,
firstItemIndex: number,
queryOperationOptions?: QueryOperationOptions,
): Promise<QueryResults> => {
const entityName = getEntityName();
const clearMessage = logConsoleProgress(`Querying ${entityName} for container ${resourceName}`);
try {
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex);
const result: QueryResults = await nextPage(documentsIterator, firstItemIndex, queryOperationOptions);
const itemCount = (result.documents && result.documents.length) || 0;
logConsoleInfo(`Successfully fetched ${itemCount} ${entityName} for container ${resourceName}`);
return result;

View File

@@ -12,13 +12,13 @@ import { handleError } from "../ErrorHandlingUtils";
import { readOfferWithSDK } from "./readOfferWithSDK";
export const readCollectionOffer = async (params: ReadCollectionOfferParams): Promise<Offer> => {
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
if (isFabric()) {
// Not exposing offers in Fabric
return undefined;
}
const clearMessage = logConsoleProgress(`Querying offer for collection ${params.collectionId}`);
try {
if (
userContext.authType === AuthType.AAD &&

View File

@@ -126,5 +126,12 @@ async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Co
throw new Error(`Unsupported default experience type: ${apiType}`);
}
return rpResponse?.value?.map((collection) => collection.properties?.resource as DataModels.Collection);
// TO DO: Remove when we get RP API Spec with materializedViews
/* eslint-disable @typescript-eslint/no-explicit-any */
return rpResponse?.value?.map((collection: any) => {
const collectionDataModel: DataModels.Collection = collection.properties?.resource as DataModels.Collection;
collectionDataModel.materializedViews = collection.properties?.resource?.materializedViews;
collectionDataModel.materializedViewDefinition = collection.properties?.resource?.materializedViewDefinition;
return collectionDataModel;
});
}

View File

@@ -1,6 +1,5 @@
import { Item, RequestOptions } from "@azure/cosmos";
import { HttpHeaders } from "Common/Constants";
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
import { CollectionBase } from "../../Contracts/ViewModels";
import DocumentId from "../../Explorer/Tree/DocumentId";
import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
@@ -24,17 +23,10 @@ export const updateDocument = async (
[HttpHeaders.partitionKey]: documentId.partitionKeyValue,
}
: {};
// If user has chosen to ignore partition key on update, pass null instead of actual partition key value
const ignorePartitionKeyOnDocumentUpdateFlag = LocalStorageUtility.getEntryBoolean(
StorageKey.IgnorePartitionKeyOnDocumentUpdate,
);
const partitionKey = ignorePartitionKeyOnDocumentUpdateFlag ? undefined : getPartitionKeyValue(documentId);
const response = await client()
.database(collection.databaseId)
.container(collection.id())
.item(documentId.id(), partitionKey)
.item(documentId.id(), getPartitionKeyValue(documentId))
.replace(newDocument, options);
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);

View File

@@ -1,15 +1,21 @@
import { CassandraProxyEndpoints, JunoEndpoints, MongoProxyEndpoints, PortalBackendEndpoints } from "Common/Constants";
import {
BackendApi,
CassandraProxyEndpoints,
JunoEndpoints,
MongoProxyEndpoints,
PortalBackendEndpoints,
} from "Common/Constants";
import {
allowedAadEndpoints,
allowedArcadiaEndpoints,
allowedEmulatorEndpoints,
allowedGraphEndpoints,
allowedHostedExplorerEndpoints,
allowedJunoOrigins,
allowedMsalRedirectEndpoints,
defaultAllowedAadEndpoints,
defaultAllowedArmEndpoints,
defaultAllowedBackendEndpoints,
defaultAllowedCassandraProxyEndpoints,
defaultAllowedGraphEndpoints,
defaultAllowedMongoProxyEndpoints,
validateEndpoint,
} from "Utils/EndpointUtils";
@@ -23,8 +29,6 @@ export enum Platform {
export interface ConfigContext {
platform: Platform;
allowedAadEndpoints: ReadonlyArray<string>;
allowedGraphEndpoints: ReadonlyArray<string>;
allowedArmEndpoints: ReadonlyArray<string>;
allowedBackendEndpoints: ReadonlyArray<string>;
allowedCassandraProxyEndpoints: ReadonlyArray<string>;
@@ -33,8 +37,10 @@ export interface ConfigContext {
gitSha?: string;
proxyPath?: string;
AAD_ENDPOINT: string;
ARM_AUTH_AREA: string;
ARM_ENDPOINT: string;
EMULATOR_ENDPOINT?: string;
ARM_API_VERSION: string;
GRAPH_ENDPOINT: string;
GRAPH_API_VERSION: string;
// This is the endpoint to get offering Ids to be used to fetch prices. Refer to this doc: https://learn.microsoft.com/en-us/rest/api/marketplacecatalog/dataplane/skus/list?view=rest-marketplacecatalog-dataplane-2023-05-01-preview&tabs=HTTP
@@ -44,24 +50,27 @@ export interface ConfigContext {
ARCADIA_ENDPOINT: string;
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
PORTAL_BACKEND_ENDPOINT: string;
NEW_BACKEND_APIS?: BackendApi[];
MONGO_PROXY_ENDPOINT: string;
CASSANDRA_PROXY_ENDPOINT: string;
NEW_CASSANDRA_APIS?: string[];
PROXY_PATH?: string;
JUNO_ENDPOINT: string;
GITHUB_CLIENT_ID: string;
GITHUB_TEST_ENV_CLIENT_ID: string;
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
isTerminalEnabled: boolean;
isPhoenixEnabled: boolean;
hostedExplorerURL: string;
armAPIVersion?: string;
msalRedirectURI?: string;
globallyEnabledCassandraAPIs?: string[];
globallyEnabledMongoAPIs?: string[];
}
// Default configuration
let configContext: Readonly<ConfigContext> = {
platform: Platform.Portal,
allowedAadEndpoints: defaultAllowedAadEndpoints,
allowedGraphEndpoints: defaultAllowedGraphEndpoints,
allowedArmEndpoints: defaultAllowedArmEndpoints,
allowedBackendEndpoints: defaultAllowedBackendEndpoints,
allowedCassandraProxyEndpoints: defaultAllowedCassandraProxyEndpoints,
@@ -76,12 +85,17 @@ let configContext: Readonly<ConfigContext> = {
`^https:\\/\\/cosmos-db-dataexplorer-germanycentral\\.azurewebsites\\.de$`,
`^https:\\/\\/.*\\.fabric\\.microsoft\\.com$`,
`^https:\\/\\/.*\\.powerbi\\.com$`,
`^https:\\/\\/.*\\.analysis-df\\.net$`,
`^https:\\/\\/.*\\.analysis-df\\.windows\\.net$`,
`^https:\\/\\/.*\\.azure-test\\.net$`,
`^https:\\/\\/dataexplorer-preview\\.azurewebsites\\.net$`,
], // Webpack injects this at build time
gitSha: process.env.GIT_SHA,
hostedExplorerURL: "https://cosmos.azure.com/",
AAD_ENDPOINT: "https://login.microsoftonline.com/",
ARM_AUTH_AREA: "https://management.azure.com/",
ARM_ENDPOINT: "https://management.azure.com/",
ARM_API_VERSION: "2016-06-01",
GRAPH_ENDPOINT: "https://graph.microsoft.com",
GRAPH_API_VERSION: "1.6",
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
@@ -95,7 +109,11 @@ let configContext: Readonly<ConfigContext> = {
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
NEW_CASSANDRA_APIS: ["postQuery", "createOrDelete", "getKeys", "getSchema"],
isTerminalEnabled: false,
isPhoenixEnabled: false,
globallyEnabledCassandraAPIs: [],
globallyEnabledMongoAPIs: [],
};
export function resetConfigContext(): void {
@@ -110,38 +128,19 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
return;
}
if (newContext.allowedAadEndpoints) {
Object.assign(configContext, { allowedAadEndpoints: newContext.allowedAadEndpoints });
}
if (newContext.allowedArmEndpoints) {
Object.assign(configContext, { allowedArmEndpoints: newContext.allowedArmEndpoints });
}
if (newContext.allowedGraphEndpoints) {
Object.assign(configContext, { allowedGraphEndpoints: newContext.allowedGraphEndpoints });
}
if (newContext.allowedBackendEndpoints) {
Object.assign(configContext, { allowedBackendEndpoints: newContext.allowedBackendEndpoints });
}
if (newContext.allowedMongoProxyEndpoints) {
Object.assign(configContext, { allowedMongoProxyEndpoints: newContext.allowedMongoProxyEndpoints });
}
if (newContext.allowedCassandraProxyEndpoints) {
Object.assign(configContext, { allowedCassandraProxyEndpoints: newContext.allowedCassandraProxyEndpoints });
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, configContext.allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints)) {
if (!validateEndpoint(newContext.ARM_ENDPOINT, configContext.allowedArmEndpoints || defaultAllowedArmEndpoints)) {
delete newContext.ARM_ENDPOINT;
}
if (!validateEndpoint(newContext.AAD_ENDPOINT, allowedAadEndpoints)) {
delete newContext.AAD_ENDPOINT;
}
if (!validateEndpoint(newContext.EMULATOR_ENDPOINT, allowedEmulatorEndpoints)) {
delete newContext.EMULATOR_ENDPOINT;
}
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, configContext.allowedGraphEndpoints)) {
if (!validateEndpoint(newContext.GRAPH_ENDPOINT, allowedGraphEndpoints)) {
delete newContext.GRAPH_ENDPOINT;
}
@@ -149,15 +148,21 @@ export function updateConfigContext(newContext: Partial<ConfigContext>): void {
delete newContext.ARCADIA_ENDPOINT;
}
if (!validateEndpoint(newContext.PORTAL_BACKEND_ENDPOINT, configContext.allowedBackendEndpoints)) {
delete newContext.PORTAL_BACKEND_ENDPOINT;
}
if (!validateEndpoint(newContext.MONGO_PROXY_ENDPOINT, configContext.allowedMongoProxyEndpoints)) {
if (
!validateEndpoint(
newContext.MONGO_PROXY_ENDPOINT,
configContext.allowedMongoProxyEndpoints || defaultAllowedMongoProxyEndpoints,
)
) {
delete newContext.MONGO_PROXY_ENDPOINT;
}
if (!validateEndpoint(newContext.CASSANDRA_PROXY_ENDPOINT, configContext.allowedCassandraProxyEndpoints)) {
if (
!validateEndpoint(
newContext.CASSANDRA_PROXY_ENDPOINT,
configContext.allowedCassandraProxyEndpoints || defaultAllowedCassandraProxyEndpoints,
)
) {
delete newContext.CASSANDRA_PROXY_ENDPOINT;
}

View File

@@ -23,7 +23,6 @@ export enum PaneKind {
GlobalSettings,
AdHocAccess,
SwitchDirectory,
QuickStart,
}
/**

View File

@@ -1,5 +1,4 @@
import { FabricMessageTypes } from "./FabricMessageTypes";
import { MessageTypes } from "./MessageTypes";
// This is the current version of these messages
export const DATA_EXPLORER_RPC_VERSION = "3";
@@ -20,32 +19,9 @@ export type DataExploreMessageV3 =
type: FabricMessageTypes.GetAllResourceTokens;
id: string;
}
| {
type: FabricMessageTypes.GetAccessToken;
id: string;
}
| {
type: MessageTypes.TelemetryInfo;
data: {
action: string;
actionModifier: string;
data: unknown;
timestamp: number;
};
}
| {
type: FabricMessageTypes.OpenSettings;
params: [{ settingsId?: "About" | "Connection" }];
}
| {
type: FabricMessageTypes.RestoreContainer;
params: [];
}
| {
type: FabricMessageTypes.ContainerUpdated;
params: {
updateType: "created" | "deleted" | "settings";
};
settingsId: string;
};
export interface GetCosmosTokenMessageOptions {
verb: "connect" | "delete" | "get" | "head" | "options" | "patch" | "post" | "put" | "trace";

View File

@@ -7,37 +7,17 @@ export interface ArmEntity {
type: string;
kind: string;
tags?: Tags;
resourceGroup?: string;
}
export interface DatabaseAccountUserAssignedIdentity {
[key: string]: {
principalId: string;
clientId: string;
};
}
export interface DatabaseAccountIdentity {
type: string;
principalId?: string;
tenantId?: string;
userAssignedIdentities?: DatabaseAccountUserAssignedIdentity;
}
export interface DatabaseAccount extends ArmEntity {
properties: DatabaseAccountExtendedProperties;
systemData?: DatabaseAccountSystemData;
identity?: DatabaseAccountIdentity | null;
}
export interface DatabaseAccountSystemData {
createdAt: string;
}
export interface DatabaseAccountBackupPolicy {
type: string;
}
export interface DatabaseAccountExtendedProperties {
documentEndpoint?: string;
disableLocalAuth?: boolean;
@@ -48,8 +28,6 @@ export interface DatabaseAccountExtendedProperties {
capabilities?: Capability[];
enableMultipleWriteLocations?: boolean;
mongoEndpoint?: string;
backupPolicy?: DatabaseAccountBackupPolicy;
defaultIdentity?: string;
readLocations?: DatabaseAccountResponseLocation[];
writeLocations?: DatabaseAccountResponseLocation[];
enableFreeTier?: boolean;
@@ -65,7 +43,6 @@ export interface DatabaseAccountExtendedProperties {
publicNetworkAccess?: string;
enablePriorityBasedExecution?: boolean;
vcoreMongoEndpoint?: string;
enableAllVersionsAndDeletesChangeFeed?: boolean;
}
export interface DatabaseAccountResponseLocation {
@@ -123,24 +100,6 @@ export interface Subscription {
authorizationSource?: string;
}
export interface DatabaseModel extends ArmEntity {
properties: DatabaseGetProperties;
}
export interface DatabaseGetProperties {
resource: DatabaseResource & ExtendedResourceProperties;
}
export interface DatabaseResource {
id: string;
}
export interface ExtendedResourceProperties {
readonly _rid?: string;
readonly _self?: string;
readonly _ts?: number;
readonly _etag?: string;
}
export interface SubscriptionPolicies {
locationPlacementId: string;
quotaId: string;
@@ -203,7 +162,6 @@ export interface Collection extends Resource {
geospatialConfig?: GeospatialConfig;
vectorEmbeddingPolicy?: VectorEmbeddingPolicy;
fullTextPolicy?: FullTextPolicy;
dataMaskingPolicy?: DataMaskingPolicy;
schema?: ISchema;
requestSchema?: () => void;
computedProperties?: ComputedProperties;
@@ -268,17 +226,6 @@ export interface ComputedProperty {
export type ComputedProperties = ComputedProperty[];
export interface DataMaskingPolicy {
includedPaths: Array<{
path: string;
strategy: string;
startPosition: number;
length: number;
}>;
excludedPaths: string[];
isPolicyEnabled: boolean;
}
export interface MaterializedView {
id: string;
_rid: string;
@@ -441,7 +388,7 @@ export interface VectorEmbeddingPolicy {
}
export interface VectorEmbedding {
dataType: "float32" | "uint8" | "int8";
dataType: "float16" | "float32" | "uint8" | "int8";
dimensions: number;
distanceFunction: "euclidean" | "cosine" | "dotproduct";
path: string;

View File

@@ -7,8 +7,6 @@ export enum FabricMessageTypes {
GetAccessToken = "GetAccessToken",
Ready = "Ready",
OpenSettings = "OpenSettings",
RestoreContainer = "RestoreContainer",
ContainerUpdated = "ContainerUpdated",
}
export interface AuthorizationToken {

View File

@@ -49,6 +49,4 @@ export enum MessageTypes {
Ready, // unused. Can be removed if the portal uses the same list of enums.
OpenCESCVAFeedbackBlade,
ActivateTab,
OpenContainerCopyFeedbackBlade,
UpdateTheme,
}

View File

@@ -1,5 +1,4 @@
import {
ItemDefinition,
JSONObject,
QueryMetrics,
Resource,
@@ -7,7 +6,7 @@ import {
TriggerDefinition,
UserDefinedFunctionDefinition,
} from "@azure/cosmos";
import type Explorer from "../Explorer/Explorer";
import Explorer from "../Explorer/Explorer";
import { ConsoleData } from "../Explorer/Menus/NotificationConsole/ConsoleData";
import { CassandraTableKey, CassandraTableKeys } from "../Explorer/Tables/TableDataClient";
import ConflictId from "../Explorer/Tree/ConflictId";
@@ -31,11 +30,8 @@ export interface UploadDetailsRecord {
numFailed: number;
numThrottled: number;
errors: string[];
resources?: ItemDefinition[];
}
export type BulkInsertResult = Omit<UploadDetailsRecord, "fileName">;
export interface QueryResultsMetadata {
hasMoreResults: boolean;
firstItemIndex: number;
@@ -50,7 +46,6 @@ export interface QueryResults extends QueryResultsMetadata {
roundTrips?: number;
headers?: any;
queryMetrics?: QueryMetrics;
ruThresholdExceeded?: boolean;
}
export interface Button {
@@ -140,7 +135,6 @@ export interface Collection extends CollectionBase {
requestSchema?: () => void;
vectorEmbeddingPolicy: ko.Observable<DataModels.VectorEmbeddingPolicy>;
fullTextPolicy: ko.Observable<DataModels.FullTextPolicy>;
dataMaskingPolicy: ko.Observable<DataModels.DataMaskingPolicy>;
indexingPolicy: ko.Observable<DataModels.IndexingPolicy>;
uniqueKeyPolicy: DataModels.UniqueKeyPolicy;
usageSizeInKB: ko.Observable<number>;
@@ -444,12 +438,6 @@ export interface DataExplorerInputsFrame {
[key: string]: string;
};
feedbackPolicies?: any;
aadToken?: string;
containerCopyEnabled?: boolean;
sessionId?: string;
theme?: {
mode: number;
};
}
export interface SelfServeFrameInputs {
@@ -483,6 +471,3 @@ export interface DropdownOption<T> {
value: T;
disable?: boolean;
}
// Remove the duplicate Explorer interface and export the type
export type { Explorer };

View File

@@ -0,0 +1,11 @@
<!doctype html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="data:," />
</head>
<body>
<div id="heatmap"></div>
</body>
</html>

View File

@@ -0,0 +1,55 @@
@import "../../../less/Common/Constants";
html {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
body {
font-family: @DataExplorerFont;
padding: 0px;
margin: 0px;
border: 0px;
overflow: hidden;
}
#heatmap {
.dark-theme {
color: @BaseLight;
}
.chartTitle {
position: absolute;
top: 5px;
left: 3px;
font-size: 13px;
}
.noDataMessage {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 10000;
height: 100%;
width: 100%;
top: 0;
left: 0;
opacity: 0.97;
div {
border-color: rgba(204, 204, 204, 0.8);
box-shadow: 0 4px 6px 0 rgba(0, 0, 0, 0.12);
padding: 15px 10px;
width: calc(55% - 40px);
font-size: 13px;
text-align: center;
border-width: 1px;
border-style: solid;
}
}
}

View File

@@ -0,0 +1,143 @@
import dayjs from "dayjs";
import { handleMessage, Heatmap, isDarkTheme } from "./Heatmap";
import { PortalTheme } from "./HeatmapDatatypes";
describe("The Heatmap Control", () => {
const dataPoints = {
"1": {
"2019-06-19T00:59:10Z": {
"Normalized Throughput": 0.35,
},
"2019-06-19T00:48:10Z": {
"Normalized Throughput": 0.25,
},
},
};
const chartCaptions = {
chartTitle: "chart title",
yAxisTitle: "YAxisTitle",
tooltipText: "Tooltip text",
timeWindow: 123456789,
};
let heatmap: Heatmap;
const theme: PortalTheme = 1;
const divElement = `<div id="${Heatmap.elementId}"></div>`;
describe("drawHeatmap rendering", () => {
beforeEach(() => {
heatmap = new Heatmap(dataPoints, chartCaptions, theme);
document.body.innerHTML = divElement;
});
afterEach(() => {
document.body.innerHTML = ``;
});
it("should call _getChartSettings when drawHeatmap is invoked", () => {
const _getChartSettings = jest.spyOn(heatmap, "_getChartSettings");
heatmap.drawHeatmap();
expect(_getChartSettings).toHaveBeenCalled();
});
it("should call _getLayoutSettings when drawHeatmap is invoked", () => {
const _getLayoutSettings = jest.spyOn(heatmap, "_getLayoutSettings");
heatmap.drawHeatmap();
expect(_getLayoutSettings).toHaveBeenCalled();
});
it("should call _getChartDisplaySettings when drawHeatmap is invoked", () => {
const _getChartDisplaySettings = jest.spyOn(heatmap, "_getChartDisplaySettings");
heatmap.drawHeatmap();
expect(_getChartDisplaySettings).toHaveBeenCalled();
});
it("drawHeatmap should render a Heatmap inside the div element", () => {
heatmap.drawHeatmap();
expect(document.body.innerHTML).not.toEqual(divElement);
});
});
describe("generateMatrixFromMap", () => {
it("should massage input data to match output expected", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).yAxisPoints).toEqual(["1"]);
expect(heatmap.generateMatrixFromMap(dataPoints).dataPoints).toEqual([[0.25, 0.35]]);
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints.length).toEqual(2);
});
it("should output the date format to ISO8601 string format", () => {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(10, 11)).toEqual("T");
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints[0].slice(-1)).toEqual("Z");
});
it("should convert the time to the user's local time", () => {
if (dayjs().utcOffset()) {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).not.toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
} else {
expect(heatmap.generateMatrixFromMap(dataPoints).xAxisPoints).toEqual([
"2019-06-19T00:48:10Z",
"2019-06-19T00:59:10Z",
]);
}
});
});
describe("isDarkTheme", () => {
it("isDarkTheme should return the correct result", () => {
expect(isDarkTheme(PortalTheme.dark)).toEqual(true);
expect(isDarkTheme(PortalTheme.azure)).not.toEqual(true);
});
});
});
describe("iframe rendering when there is no data", () => {
afterEach(() => {
document.body.innerHTML = ``;
});
it("should show a no data message with a dark theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 4,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
it("should show a no data message with a white theme", () => {
const data = {
data: {
signature: "pcIframe",
data: {
chartData: {},
chartSettings: {},
theme: 2,
},
},
origin: "http://localhost",
};
const divElement = `<div id="${Heatmap.elementId}"></div>`;
document.body.innerHTML = divElement;
handleMessage(data as MessageEvent);
expect(document.body.innerHTML).not.toContain("dark-theme");
expect(document.body.innerHTML).toContain("noDataMessage");
});
});

View File

@@ -0,0 +1,272 @@
import dayjs from "dayjs";
import * as Plotly from "plotly.js-cartesian-dist-min";
import { sendCachedDataMessage, sendReadyMessage } from "../../Common/MessageHandler";
import { StyleConstants } from "../../Common/StyleConstants";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { isInvalidParentFrameOrigin } from "../../Utils/MessageValidation";
import "./Heatmap.less";
import {
ChartSettings,
DataPayload,
DisplaySettings,
FontSettings,
HeatmapCaptions,
HeatmapData,
LayoutSettings,
PartitionTimeStampToData,
PortalTheme,
} from "./HeatmapDatatypes";
export class Heatmap {
public static readonly elementId: string = "heatmap";
private _chartData: HeatmapData;
private _heatmapCaptions: HeatmapCaptions;
private _theme: PortalTheme;
private _defaultFontColor: string;
constructor(data: DataPayload, heatmapCaptions: HeatmapCaptions, theme: PortalTheme) {
this._theme = theme;
this._defaultFontColor = StyleConstants.BaseDark;
this._setThemeColorForChart();
this._chartData = this.generateMatrixFromMap(data);
this._heatmapCaptions = heatmapCaptions;
}
private _setThemeColorForChart() {
if (isDarkTheme(this._theme)) {
this._defaultFontColor = StyleConstants.BaseLight;
}
}
private _getFontStyles(size: number = StyleConstants.MediumFontSize, color = "#838383"): FontSettings {
return {
family: StyleConstants.DataExplorerFont,
size,
color,
};
}
public generateMatrixFromMap(data: DataPayload): HeatmapData {
// all keys in data payload, sorted...
const rows: string[] = Object.keys(data).sort((a: string, b: string) => {
if (parseInt(a) < parseInt(b)) {
return -1;
} else {
if (parseInt(a) > parseInt(b)) {
return 1;
} else {
return 0;
}
}
});
const output: HeatmapData = {
yAxisPoints: [],
dataPoints: [],
xAxisPoints: Object.keys(data[rows[0]]).sort((a: string, b: string) => {
if (a < b) {
return -1;
} else {
if (a > b) {
return 1;
} else {
return 0;
}
}
}),
};
// go thru all rows and create 2d matrix for heatmap...
for (let i = 0; i < rows.length; i++) {
output.yAxisPoints.push(rows[i]);
const dataPoints: number[] = [];
for (let a = 0; a < output.xAxisPoints.length; a++) {
const row: PartitionTimeStampToData = data[rows[i]];
dataPoints.push(row[output.xAxisPoints[a]]["Normalized Throughput"]);
}
output.dataPoints.push(dataPoints);
}
for (let a = 0; a < output.xAxisPoints.length; a++) {
const dateTime = output.xAxisPoints[a];
// convert to local users timezone...
const day = dayjs(new Date(dateTime)).format("YYYY-MM-DD");
const hour = dayjs(new Date(dateTime)).format("HH:mm:ss");
// coerce to ISOString format since that is what plotly wants...
output.xAxisPoints[a] = `${day}T${hour}Z`;
}
return output;
}
// public for testing purposes
public _getChartSettings(): ChartSettings[] {
return [
{
z: this._chartData.dataPoints,
type: "heatmap",
zmin: 0,
zmid: 50,
zmax: 100,
colorscale: [
[0.0, "#1FD338"],
[0.1, "#1CAD2F"],
[0.2, "#50A527"],
[0.3, "#719F21"],
[0.4, "#95991B"],
[0.5, "#CE8F11"],
[0.6, "#E27F0F"],
[0.7, "#E46612"],
[0.8, "#E64914"],
[0.9, "#B80016"],
[1.0, "#B80016"],
],
name: "",
hovertemplate: this._heatmapCaptions.tooltipText,
colorbar: {
thickness: 15,
outlinewidth: 0,
tickcolor: StyleConstants.BaseDark,
tickfont: this._getFontStyles(10, this._defaultFontColor),
},
y: this._chartData.yAxisPoints,
x: this._chartData.xAxisPoints,
},
];
}
// public for testing purposes
public _getLayoutSettings(): LayoutSettings {
return {
margin: {
l: 40,
r: 10,
b: 35,
t: 30,
pad: 0,
},
paper_bgcolor: "transparent",
plot_bgcolor: "transparent",
width: 462,
height: 240,
yaxis: {
title: this._heatmapCaptions.yAxisTitle,
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
fixedrange: true,
ticks: "",
showticklabels: false,
},
xaxis: {
fixedrange: true,
title: "*White area in heatmap indicates there is no available data",
titlefont: this._getFontStyles(11),
autorange: true,
showgrid: false,
zeroline: false,
showline: false,
autotick: true,
tickformat: this._heatmapCaptions.timeWindow > 7 ? "%I:%M %p" : "%b %e",
showticklabels: true,
tickfont: this._getFontStyles(10),
},
title: {
text: this._heatmapCaptions.chartTitle,
x: 0.01,
font: this._getFontStyles(13, this._defaultFontColor),
},
};
}
// public for testing purposes
public _getChartDisplaySettings(): DisplaySettings {
return {
/* heatmap can be fully responsive however the min-height needed in that case is greater than the iframe portal height, hence explicit width + height have been set in _getLayoutSettings
responsive: true,*/
displayModeBar: false,
};
}
public drawHeatmap(): void {
// todo - create random elementId generator so multiple heatmaps can be created - ticket # 431469
Plotly.plot(
Heatmap.elementId,
this._getChartSettings(),
this._getLayoutSettings(),
this._getChartDisplaySettings(),
);
const plotDiv: any = document.getElementById(Heatmap.elementId);
plotDiv.on("plotly_click", (data: any) => {
let timeSelected: string = data.points[0].x;
timeSelected = timeSelected.replace(" ", "T");
timeSelected = `${timeSelected}Z`;
let xAxisIndex = 0;
for (let i = 0; i < this._chartData.xAxisPoints.length; i++) {
if (this._chartData.xAxisPoints[i] === timeSelected) {
xAxisIndex = i;
break;
}
}
const output = [];
for (let i = 0; i < this._chartData.dataPoints.length; i++) {
output.push(this._chartData.dataPoints[i][xAxisIndex]);
}
sendCachedDataMessage(MessageTypes.LogInfo, output);
});
}
}
export function isDarkTheme(theme: PortalTheme) {
return theme === PortalTheme.dark;
}
export function handleMessage(event: MessageEvent) {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (typeof event.data !== "object" || event.data["signature"] !== "pcIframe") {
return;
}
if (
typeof event.data.data !== "object" ||
!("chartData" in event.data.data) ||
!("chartSettings" in event.data.data)
) {
return;
}
Plotly.purge(Heatmap.elementId);
document.getElementById(Heatmap.elementId)!.innerHTML = "";
const data = event.data.data;
const chartData: DataPayload = data.chartData;
const chartSettings: HeatmapCaptions = data.chartSettings;
const chartTheme: PortalTheme = data.theme;
if (Object.keys(chartData).length) {
new Heatmap(chartData, chartSettings, chartTheme).drawHeatmap();
} else {
const chartTitleElement = document.createElement("div");
chartTitleElement.innerHTML = data.chartSettings.chartTitle;
chartTitleElement.classList.add("chartTitle");
const noDataMessageElement = document.createElement("div");
noDataMessageElement.classList.add("noDataMessage");
const noDataMessageContent = document.createElement("div");
noDataMessageContent.innerHTML = data.errorMessage;
noDataMessageElement.appendChild(noDataMessageContent);
if (isDarkTheme(chartTheme)) {
chartTitleElement.classList.add("dark-theme");
noDataMessageElement.classList.add("dark-theme");
noDataMessageContent.classList.add("dark-theme");
}
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);
}
}
window.addEventListener("message", handleMessage, false);
sendReadyMessage();

View File

@@ -0,0 +1,106 @@
type dataPoint = string | number;
export interface DataPayload {
[id: string]: PartitionTimeStampToData;
}
export enum PortalTheme {
blue = 1,
azure,
light,
dark,
}
export interface HeatmapData {
yAxisPoints: string[];
xAxisPoints: string[];
dataPoints: dataPoint[][];
}
export interface HeatmapCaptions {
chartTitle: string;
yAxisTitle: string;
tooltipText: string;
timeWindow: number;
}
export interface FontSettings {
family: string;
size: number;
color: string;
}
export interface LayoutSettings {
paper_bgcolor?: string;
plot_bgcolor?: string;
margin?: {
l: number;
r: number;
b: number;
t: number;
pad: number;
};
width?: number;
height?: number;
yaxis?: {
fixedrange: boolean;
title: HeatmapCaptions["yAxisTitle"];
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
ticks: "";
showticklabels: boolean;
};
xaxis?: {
fixedrange: boolean;
title: string;
titlefont: FontSettings;
autorange: boolean;
showgrid: boolean;
zeroline: boolean;
showline: boolean;
autotick: boolean;
showticklabels: boolean;
tickformat: string;
tickfont: FontSettings;
};
title?: {
text: HeatmapCaptions["chartTitle"];
x: number;
font?: FontSettings;
};
font?: FontSettings;
}
export interface ChartSettings {
z: HeatmapData["dataPoints"];
type: "heatmap";
zmin: number;
zmid: number;
zmax: number;
colorscale: [number, string][];
name: string;
hovertemplate: HeatmapCaptions["tooltipText"];
colorbar: {
thickness: number;
outlinewidth: number;
tickcolor: string;
tickfont: FontSettings;
};
y: HeatmapData["yAxisPoints"];
x: HeatmapData["xAxisPoints"];
}
export interface DisplaySettings {
displayModeBar: boolean;
responsive?: boolean;
}
export interface PartitionTimeStampToData {
[timeSeriesDates: string]: {
[NormalizedThroughput: string]: number;
};
}

View File

@@ -1,717 +0,0 @@
import "@testing-library/jest-dom";
import Explorer from "Explorer/Explorer";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
import * as Logger from "../../../Common/Logger";
import { useSidePanel } from "../../../hooks/useSidePanel";
import * as dataTransferService from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
import * as CopyJobUtils from "../CopyJobUtils";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
import CopyJobDetails from "../MonitorCopyJobs/Components/CopyJobDetails";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobContextState, CopyJobType } from "../Types/CopyJobTypes";
import {
getCopyJobs,
openCopyJobDetailsPanel,
openCreateCopyJobPanel,
submitCreateCopyJob,
updateCopyJobStatus,
} from "./CopyJobActions";
jest.mock("UserContext", () => ({
userContext: {
databaseAccount: {
id: "/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
},
},
}));
jest.mock("../../../hooks/useSidePanel");
jest.mock("../../../Common/Logger");
jest.mock("../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs");
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
jest.mock("../CopyJobUtils");
jest.mock("../../../Common/dataAccess/dataTransfers");
describe("CopyJobActions", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("openCreateCopyJobPanel", () => {
it("should open side panel with correct parameters", () => {
const mockExplorer = {} as Explorer;
const mockSetPanelHasConsole = jest.fn();
const mockOpenSidePanel = jest.fn();
(useSidePanel.getState as jest.Mock).mockReturnValue({
setPanelHasConsole: mockSetPanelHasConsole,
openSidePanel: mockOpenSidePanel,
});
openCreateCopyJobPanel(mockExplorer);
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.any(String), expect.any(Object), "650px");
});
it("should render CreateCopyJobScreensProvider in side panel", () => {
const mockExplorer = {} as Explorer;
const mockOpenSidePanel = jest.fn();
(useSidePanel.getState as jest.Mock).mockReturnValue({
setPanelHasConsole: jest.fn(),
openSidePanel: mockOpenSidePanel,
});
openCreateCopyJobPanel(mockExplorer);
const sidePanelContent = mockOpenSidePanel.mock.calls[0][1];
expect(sidePanelContent.type).toBe(CreateCopyJobScreensProvider);
expect(sidePanelContent.props.explorer).toBe(mockExplorer);
});
});
describe("openCopyJobDetailsPanel", () => {
it("should open side panel with job details", () => {
const mockJob: CopyJobType = {
ID: "1",
Mode: "online",
Name: "test-job",
Status: CopyJobStatusType.InProgress,
CompletionPercentage: 50,
Duration: "01 hours, 30 minutes, 45 seconds",
LastUpdatedTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
Source: {
component: "CosmosDBSql",
databaseName: "source-db",
containerName: "source-container",
},
Destination: {
component: "CosmosDBSql",
databaseName: "target-db",
containerName: "target-container",
},
};
const mockSetPanelHasConsole = jest.fn();
const mockOpenSidePanel = jest.fn();
(useSidePanel.getState as jest.Mock).mockReturnValue({
setPanelHasConsole: mockSetPanelHasConsole,
openSidePanel: mockOpenSidePanel,
});
openCopyJobDetailsPanel(mockJob);
expect(mockSetPanelHasConsole).toHaveBeenCalledWith(false);
expect(mockOpenSidePanel).toHaveBeenCalledWith(expect.stringContaining("test-job"), expect.any(Object), "650px");
});
it("should render CopyJobDetails component with correct job", () => {
const mockJob: CopyJobType = {
ID: "1",
Mode: "offline",
Name: "test-job-2",
Status: CopyJobStatusType.Completed,
CompletionPercentage: 100,
Duration: "02 hours, 15 minutes, 30 seconds",
LastUpdatedTime: "1/2/2025, 11:00:00 AM",
timestamp: 1704193200000,
Source: {
component: "CosmosDBSql",
databaseName: "source-db",
containerName: "source-container",
},
Destination: {
component: "CosmosDBSql",
databaseName: "target-db",
containerName: "target-container",
},
};
const mockOpenSidePanel = jest.fn();
(useSidePanel.getState as jest.Mock).mockReturnValue({
setPanelHasConsole: jest.fn(),
openSidePanel: mockOpenSidePanel,
});
openCopyJobDetailsPanel(mockJob);
const sidePanelContent = mockOpenSidePanel.mock.calls[0][1];
expect(sidePanelContent.type).toBe(CopyJobDetails);
expect(sidePanelContent.props.job).toBe(mockJob);
});
});
describe("getCopyJobs", () => {
beforeEach(() => {
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
subscriptionId: "sub-123",
resourceGroup: "rg-test",
accountName: "test-account",
});
});
it("should fetch and format copy jobs successfully", async () => {
const mockResponse = [
{
properties: {
jobName: "job-1",
status: "InProgress",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 50,
totalCount: 100,
mode: "online",
duration: "01:30:45",
source: {
component: "CosmosDBSql",
databaseName: "source-db",
containerName: "source-container",
},
destination: {
component: "CosmosDBSql",
databaseName: "target-db",
containerName: "target-container",
},
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours, 30 minutes, 45 seconds");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("InProgress");
const result = await getCopyJobs();
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
ID: "1",
Name: "job-1",
Status: "InProgress",
CompletionPercentage: 50,
Mode: "online",
});
});
it("should filter jobs by CosmosDBSql component", async () => {
const mockResponse = [
{
properties: {
jobName: "sql-job",
status: "Completed",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 100,
totalCount: 100,
mode: "offline",
duration: "02:00:00",
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
},
},
{
properties: {
jobName: "other-job",
status: "Completed",
lastUpdatedUtcTime: "2025-01-01T11:00:00Z",
processedCount: 100,
totalCount: 100,
mode: "offline",
duration: "01:00:00",
source: { component: "OtherComponent", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("02 hours");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Completed");
const result = await getCopyJobs();
expect(result).toHaveLength(1);
expect(result[0].Name).toBe("sql-job");
});
it("should sort jobs by last updated time (newest first)", async () => {
const mockResponse = [
{
properties: {
jobName: "older-job",
status: "Completed",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 100,
totalCount: 100,
mode: "offline",
duration: "01:00:00",
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
},
},
{
properties: {
jobName: "newer-job",
status: "InProgress",
lastUpdatedUtcTime: "2025-01-02T10:00:00Z",
processedCount: 50,
totalCount: 100,
mode: "online",
duration: "00:30:00",
source: { component: "CosmosDBSql", databaseName: "db3", containerName: "c3" },
destination: { component: "CosmosDBSql", databaseName: "db4", containerName: "c4" },
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Completed");
const result = await getCopyJobs();
expect(result[0].Name).toBe("newer-job");
expect(result[1].Name).toBe("older-job");
});
it("should calculate completion percentage correctly", async () => {
const mockResponse = [
{
properties: {
jobName: "job-1",
status: "InProgress",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 75,
totalCount: 100,
mode: "online",
duration: "01:00:00",
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("01 hours");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("InProgress");
const result = await getCopyJobs();
expect(result[0].CompletionPercentage).toBe(75);
});
it("should handle zero total count gracefully", async () => {
const mockResponse = [
{
properties: {
jobName: "job-1",
status: "Pending",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 0,
totalCount: 0,
mode: "online",
duration: "00:00:00",
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("0 seconds");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Pending");
const result = await getCopyJobs();
expect(result[0].CompletionPercentage).toBe(0);
});
it("should extract error messages if present", async () => {
const mockError = {
message: "Error message line 1\r\n\r\nError message line 2",
code: "ErrorCode123",
};
const mockResponse = [
{
properties: {
jobName: "failed-job",
status: "Failed",
lastUpdatedUtcTime: "2025-01-01T10:00:00Z",
processedCount: 50,
totalCount: 100,
mode: "offline",
duration: "00:30:00",
source: { component: "CosmosDBSql", databaseName: "db1", containerName: "c1" },
destination: { component: "CosmosDBSql", databaseName: "db2", containerName: "c2" },
error: mockError,
},
},
];
(getDataTransferJobs as jest.Mock).mockResolvedValue(mockResponse);
(CopyJobUtils.formatUTCDateTime as jest.Mock).mockReturnValue({
formattedDateTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
});
(CopyJobUtils.convertTime as jest.Mock).mockReturnValue("30 minutes");
(CopyJobUtils.convertToCamelCase as jest.Mock).mockReturnValue("Failed");
(CopyJobUtils.extractErrorMessage as jest.Mock).mockReturnValue({
message: "Error message line 1",
code: "ErrorCode123",
});
const result = await getCopyJobs();
expect(result[0].Error).toEqual({
message: "Error message line 1",
code: "ErrorCode123",
});
expect(CopyJobUtils.extractErrorMessage).toHaveBeenCalledWith(mockError);
});
it("should abort previous request when new request is made", async () => {
const mockAbortController = {
abort: jest.fn(),
signal: {} as AbortSignal,
};
(global as any).AbortController = jest.fn(() => mockAbortController);
(getDataTransferJobs as jest.Mock).mockResolvedValue([]);
getCopyJobs();
expect(mockAbortController.abort).not.toHaveBeenCalled();
getCopyJobs();
expect(mockAbortController.abort).toHaveBeenCalledTimes(1);
});
it("should throw error for invalid response format", async () => {
(getDataTransferJobs as jest.Mock).mockResolvedValue("not-an-array");
await expect(getCopyJobs()).rejects.toThrow("Invalid migration job status response: Expected an array of jobs.");
});
it("should handle abort signal error", async () => {
const abortError = {
message: "Aborted",
content: JSON.stringify({ message: "signal is aborted without reason" }),
};
(getDataTransferJobs as jest.Mock).mockRejectedValue(abortError);
await expect(getCopyJobs()).rejects.toMatchObject({
message: expect.stringContaining("Previous copy job request was cancelled."),
});
});
it("should handle generic errors", async () => {
const genericError = new Error("Network error");
(getDataTransferJobs as jest.Mock).mockRejectedValue(genericError);
await expect(getCopyJobs()).rejects.toThrow("Network error");
});
});
describe("submitCreateCopyJob", () => {
let mockRefreshJobList: jest.Mock;
let mockOnSuccess: jest.Mock;
beforeEach(() => {
mockRefreshJobList = jest.fn();
mockOnSuccess = jest.fn();
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
subscriptionId: "sub-123",
resourceGroup: "rg-test",
accountName: "test-account",
});
(MonitorCopyJobsRefState.getState as jest.Mock).mockReturnValue({
ref: { refreshJobList: mockRefreshJobList },
});
});
it("should create intra-account copy job successfully", async () => {
const mockState: CopyJobContextState = {
jobName: "test-job",
migrationType: "online" as any,
source: {
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
};
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
(dataTransferService.create as jest.Mock).mockResolvedValue({ id: "job-id" });
await submitCreateCopyJob(mockState, mockOnSuccess);
expect(dataTransferService.create).toHaveBeenCalledWith(
"sub-123",
"rg-test",
"test-account",
"test-job",
expect.objectContaining({
properties: expect.objectContaining({
source: expect.objectContaining({
component: "CosmosDBSql",
databaseName: "source-db",
containerName: "source-container",
}),
destination: expect.objectContaining({
component: "CosmosDBSql",
databaseName: "target-db",
containerName: "target-container",
}),
mode: "online",
}),
}),
);
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.source.remoteAccountName).toBeUndefined();
expect(mockRefreshJobList).toHaveBeenCalled();
expect(mockOnSuccess).toHaveBeenCalled();
});
it("should create inter-account copy job with source account name", async () => {
const mockState: CopyJobContextState = {
jobName: "cross-account-job",
migrationType: "offline" as any,
source: {
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "sub-456",
account: { id: "account-2", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
};
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(false);
(dataTransferService.create as jest.Mock).mockResolvedValue({ id: "job-id" });
await submitCreateCopyJob(mockState, mockOnSuccess);
const callArgs = (dataTransferService.create as jest.Mock).mock.calls[0][4];
expect(callArgs.properties.source.remoteAccountName).toBe("source-account");
expect(mockOnSuccess).toHaveBeenCalled();
});
it("should handle errors and log them", async () => {
const mockState: CopyJobContextState = {
jobName: "failing-job",
migrationType: "online" as any,
source: {
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
};
const mockError = new Error("API Error");
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
(dataTransferService.create as jest.Mock).mockRejectedValue(mockError);
await expect(submitCreateCopyJob(mockState, mockOnSuccess)).rejects.toThrow("API Error");
expect(Logger.logError).toHaveBeenCalledWith("API Error", "CopyJob/CopyJobActions.submitCreateCopyJob");
expect(mockOnSuccess).not.toHaveBeenCalled();
expect(mockRefreshJobList).not.toHaveBeenCalled();
});
it("should handle errors without message", async () => {
const mockState: CopyJobContextState = {
jobName: "test-job",
migrationType: "online" as any,
source: {
subscription: {} as any,
account: { id: "account-1", name: "source-account" } as any,
databaseId: "source-db",
containerId: "source-container",
},
target: {
subscriptionId: "sub-123",
account: { id: "account-1", name: "target-account" } as any,
databaseId: "target-db",
containerId: "target-container",
},
};
(CopyJobUtils.isIntraAccountCopy as jest.Mock).mockReturnValue(true);
(dataTransferService.create as jest.Mock).mockRejectedValue({});
await expect(submitCreateCopyJob(mockState, mockOnSuccess)).rejects.toEqual({});
expect(Logger.logError).toHaveBeenCalledWith(
"Error submitting create copy job. Please try again later.",
"CopyJob/CopyJobActions.submitCreateCopyJob",
);
});
});
describe("updateCopyJobStatus", () => {
const mockJob: CopyJobType = {
ID: "1",
Mode: "online",
Name: "test-job",
Status: CopyJobStatusType.InProgress,
CompletionPercentage: 50,
Duration: "01 hours, 30 minutes",
LastUpdatedTime: "1/1/2025, 10:00:00 AM",
timestamp: 1704106800000,
Source: {
component: "CosmosDBSql",
databaseName: "source-db",
containerName: "source-container",
},
Destination: {
component: "CosmosDBSql",
databaseName: "target-db",
containerName: "target-container",
},
};
beforeEach(() => {
(CopyJobUtils.getAccountDetailsFromResourceId as jest.Mock).mockReturnValue({
subscriptionId: "sub-123",
resourceGroup: "rg-test",
accountName: "test-account",
});
});
it("should pause a job successfully", async () => {
const mockResponse = { id: "job-id", properties: { status: "Paused" } };
(dataTransferService.pause as jest.Mock).mockResolvedValue(mockResponse);
const result = await updateCopyJobStatus(mockJob, CopyJobActions.pause);
expect(dataTransferService.pause).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
expect(result).toEqual(mockResponse);
});
it("should resume a job successfully", async () => {
const mockResponse = { id: "job-id", properties: { status: "InProgress" } };
(dataTransferService.resume as jest.Mock).mockResolvedValue(mockResponse);
const result = await updateCopyJobStatus(mockJob, CopyJobActions.resume);
expect(dataTransferService.resume).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
expect(result).toEqual(mockResponse);
});
it("should cancel a job successfully", async () => {
const mockResponse = { id: "job-id", properties: { status: "Cancelled" } };
(dataTransferService.cancel as jest.Mock).mockResolvedValue(mockResponse);
const result = await updateCopyJobStatus(mockJob, CopyJobActions.cancel);
expect(dataTransferService.cancel).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
expect(result).toEqual(mockResponse);
});
it("should complete a job successfully", async () => {
const mockResponse = { id: "job-id", properties: { status: "Completed" } };
(dataTransferService.complete as jest.Mock).mockResolvedValue(mockResponse);
const result = await updateCopyJobStatus(mockJob, CopyJobActions.complete);
expect(dataTransferService.complete).toHaveBeenCalledWith("sub-123", "rg-test", "test-account", "test-job");
expect(result).toEqual(mockResponse);
});
it("should handle case-insensitive action names", async () => {
const mockResponse = { id: "job-id", properties: { status: "Paused" } };
(dataTransferService.pause as jest.Mock).mockResolvedValue(mockResponse);
await updateCopyJobStatus(mockJob, "PAUSE");
expect(dataTransferService.pause).toHaveBeenCalled();
});
it("should throw error for unsupported action", async () => {
await expect(updateCopyJobStatus(mockJob, "invalid-action")).rejects.toThrow(
"Unsupported action: invalid-action",
);
expect(Logger.logError).toHaveBeenCalled();
});
it("should normalize error messages with status types", async () => {
const mockError = {
message: "Job must be in 'Running' or 'InProgress' state",
content: { error: "State error" },
};
(dataTransferService.pause as jest.Mock).mockRejectedValue(mockError);
await expect(updateCopyJobStatus(mockJob, CopyJobActions.pause)).rejects.toEqual(mockError);
const loggedMessage = (Logger.logError as jest.Mock).mock.calls[0][0];
expect(loggedMessage).toContain("Error updating copy job status");
});
it("should log error with correct context", async () => {
const mockError = new Error("Network failure");
(dataTransferService.resume as jest.Mock).mockRejectedValue(mockError);
await expect(updateCopyJobStatus(mockJob, CopyJobActions.resume)).rejects.toThrow("Network failure");
expect(Logger.logError).toHaveBeenCalledWith(
expect.stringContaining("Error updating copy job status"),
"CopyJob/CopyJobActions.updateCopyJobStatus",
);
});
it("should handle errors with content property", async () => {
const mockError = {
content: { message: "Content error message" },
};
(dataTransferService.cancel as jest.Mock).mockRejectedValue(mockError);
await expect(updateCopyJobStatus(mockJob, CopyJobActions.cancel)).rejects.toEqual(mockError);
expect(Logger.logError).toHaveBeenCalled();
});
});
});

View File

@@ -1,201 +0,0 @@
import Explorer from "Explorer/Explorer";
import React from "react";
import { userContext } from "UserContext";
import { getDataTransferJobs } from "../../../Common/dataAccess/dataTransfers";
import { logError } from "../../../Common/Logger";
import { useSidePanel } from "../../../hooks/useSidePanel";
import {
cancel,
complete,
create,
pause,
resume,
} from "../../../Utils/arm/generatedClients/dataTransferService/dataTransferJobs";
import {
CreateJobRequest,
DataTransferJobGetResults,
} from "../../../Utils/arm/generatedClients/dataTransferService/types";
import ContainerCopyMessages from "../ContainerCopyMessages";
import {
convertTime,
convertToCamelCase,
COSMOS_SQL_COMPONENT,
extractErrorMessage,
formatUTCDateTime,
getAccountDetailsFromResourceId,
isIntraAccountCopy,
} from "../CopyJobUtils";
import CreateCopyJobScreensProvider from "../CreateCopyJob/Screens/CreateCopyJobScreensProvider";
import { CopyJobActions, CopyJobStatusType } from "../Enums/CopyJobEnums";
import CopyJobDetails from "../MonitorCopyJobs/Components/CopyJobDetails";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobContextState, CopyJobError, CopyJobErrorType, CopyJobType } from "../Types/CopyJobTypes";
export const openCreateCopyJobPanel = (explorer: Explorer) => {
const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel(
ContainerCopyMessages.createCopyJobPanelTitle,
<CreateCopyJobScreensProvider explorer={explorer} />,
"650px",
);
};
export const openCopyJobDetailsPanel = (job: CopyJobType) => {
const sidePanelState = useSidePanel.getState();
sidePanelState.setPanelHasConsole(false);
sidePanelState.openSidePanel(
ContainerCopyMessages.copyJobDetailsPanelTitle(job.Name),
<CopyJobDetails job={job} />,
"650px",
);
};
let copyJobsAbortController: AbortController | null = null;
export const getCopyJobs = async (): Promise<CopyJobType[]> => {
try {
if (copyJobsAbortController) {
copyJobsAbortController.abort();
}
copyJobsAbortController = new AbortController();
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const jobs = await getDataTransferJobs(subscriptionId, resourceGroup, accountName, copyJobsAbortController.signal);
if (!Array.isArray(jobs)) {
throw new Error("Invalid migration job status response: Expected an array of jobs.");
}
copyJobsAbortController = null;
const calculateCompletionPercentage = (processed: number, total: number): number => {
if (
typeof processed !== "number" ||
typeof total !== "number" ||
!isFinite(processed) ||
!isFinite(total) ||
total <= 0
) {
return 0;
}
const percentage = Math.round((processed / total) * 100);
return Math.max(0, Math.min(100, percentage));
};
const formattedJobs: CopyJobType[] = jobs
.filter(
(job: DataTransferJobGetResults) =>
job.properties?.source?.component === COSMOS_SQL_COMPONENT &&
job.properties?.destination?.component === COSMOS_SQL_COMPONENT,
)
.sort(
(current: DataTransferJobGetResults, next: DataTransferJobGetResults) =>
new Date(next.properties.lastUpdatedUtcTime).getTime() -
new Date(current.properties.lastUpdatedUtcTime).getTime(),
)
.map((job: DataTransferJobGetResults, index: number) => {
const dateTimeObj = formatUTCDateTime(job.properties.lastUpdatedUtcTime);
return {
ID: (index + 1).toString(),
Mode: job.properties.mode,
Name: job.properties.jobName,
Source: job.properties.source,
Destination: job.properties.destination,
Status: convertToCamelCase(job.properties.status) as CopyJobType["Status"],
CompletionPercentage: calculateCompletionPercentage(job.properties.processedCount, job.properties.totalCount),
Duration: convertTime(job.properties.duration),
LastUpdatedTime: dateTimeObj.formattedDateTime,
timestamp: dateTimeObj.timestamp,
Error: job.properties.error ? extractErrorMessage(job.properties.error as unknown as CopyJobErrorType) : null,
} as CopyJobType;
});
return formattedJobs;
} catch (error) {
const errorContent = JSON.stringify(error.content || error.message || error);
if (errorContent.includes("signal is aborted without reason")) {
throw {
message: "Previous copy job request was cancelled.",
};
} else {
throw error;
}
}
};
export const submitCreateCopyJob = async (state: CopyJobContextState, onSuccess: () => void) => {
try {
const { source, target, migrationType, jobName } = state;
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const isSameAccount = isIntraAccountCopy(source?.account?.id, target?.account?.id);
const body = {
properties: {
source: {
component: "CosmosDBSql",
...(isSameAccount ? {} : { remoteAccountName: source?.account?.name }),
databaseName: source?.databaseId,
containerName: source?.containerId,
},
destination: {
component: "CosmosDBSql",
databaseName: target?.databaseId,
containerName: target?.containerId,
},
mode: migrationType,
},
} as unknown as CreateJobRequest;
const response = await create(subscriptionId, resourceGroup, accountName, jobName, body);
MonitorCopyJobsRefState.getState().ref?.refreshJobList();
onSuccess();
return response;
} catch (error) {
const errorMessage = error.message || "Error submitting create copy job. Please try again later.";
logError(errorMessage, "CopyJob/CopyJobActions.submitCreateCopyJob");
throw error;
}
};
export const updateCopyJobStatus = async (job: CopyJobType, action: string): Promise<DataTransferJobGetResults> => {
try {
let updateFn = null;
switch (action.toLowerCase()) {
case CopyJobActions.pause:
updateFn = pause;
break;
case CopyJobActions.resume:
updateFn = resume;
break;
case CopyJobActions.cancel:
updateFn = cancel;
break;
case CopyJobActions.complete:
updateFn = complete;
break;
default:
throw new Error(`Unsupported action: ${action}`);
}
const { subscriptionId, resourceGroup, accountName } = getAccountDetailsFromResourceId(
userContext.databaseAccount?.id || "",
);
const response = await updateFn?.(subscriptionId, resourceGroup, accountName, job.Name);
return response;
} catch (error) {
const errorMessage = JSON.stringify((error as CopyJobError).message || error.content || error);
const statusList = [CopyJobStatusType.Running, CopyJobStatusType.InProgress, CopyJobStatusType.Partitioning];
const pattern = new RegExp(`'(${statusList.join("|")})'`, "g");
const normalizedErrorMessage = errorMessage.replace(
pattern,
`'${ContainerCopyMessages.MonitorJobs.Status.InProgress}'`,
);
logError(`Error updating copy job status: ${normalizedErrorMessage}`, "CopyJob/CopyJobActions.updateCopyJobStatus");
throw error;
}
};

View File

@@ -1,185 +0,0 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import React from "react";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
import CopyJobCommandBar from "./CopyJobCommandBar";
import * as Utils from "./Utils";
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState");
jest.mock("../../Menus/CommandBar/CommandBarUtil");
jest.mock("./Utils");
describe("CopyJobCommandBar", () => {
let mockExplorer: Explorer;
let mockConvertButton: jest.MockedFunction<typeof CommandBarUtil.convertButton>;
let mockGetCommandBarButtons: jest.MockedFunction<typeof Utils.getCommandBarButtons>;
beforeEach(() => {
mockExplorer = {} as Explorer;
mockConvertButton = CommandBarUtil.convertButton as jest.MockedFunction<typeof CommandBarUtil.convertButton>;
mockGetCommandBarButtons = Utils.getCommandBarButtons as jest.MockedFunction<typeof Utils.getCommandBarButtons>;
jest.clearAllMocks();
});
it("should render without crashing", () => {
mockGetCommandBarButtons.mockReturnValue([]);
mockConvertButton.mockReturnValue([]);
const { container } = render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(container.querySelector(".commandBarContainer")).toBeInTheDocument();
});
it("should call getCommandBarButtons with explorer", () => {
mockGetCommandBarButtons.mockReturnValue([]);
mockConvertButton.mockReturnValue([]);
render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(1);
});
it("should call convertButton with command bar items and background color", () => {
const mockCommandButtonProps: CommandButtonComponentProps[] = [
{
iconSrc: "icon.svg",
iconAlt: "Test Icon",
onCommandClick: jest.fn(),
commandButtonLabel: "Test Button",
ariaLabel: "Test Button Aria Label",
tooltipText: "Test Tooltip",
hasPopup: false,
disabled: false,
},
];
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
mockConvertButton.mockReturnValue([]);
render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockConvertButton).toHaveBeenCalledTimes(1);
});
it("should render FluentCommandBar with correct aria label", () => {
mockGetCommandBarButtons.mockReturnValue([]);
mockConvertButton.mockReturnValue([]);
const { getByRole } = render(<CopyJobCommandBar explorer={mockExplorer} />);
const commandBar = getByRole("menubar", { hidden: true });
expect(commandBar).toHaveAttribute("aria-label", "Use left and right arrow keys to navigate between commands");
});
it("should render FluentCommandBar with converted items", () => {
const mockCommandButtonProps: CommandButtonComponentProps[] = [
{
iconSrc: "icon1.svg",
iconAlt: "Test Icon 1",
onCommandClick: jest.fn(),
commandButtonLabel: "Test Button 1",
ariaLabel: "Test Button 1 Aria Label",
tooltipText: "Test Tooltip 1",
hasPopup: false,
disabled: false,
},
{
iconSrc: "icon2.svg",
iconAlt: "Test Icon 2",
onCommandClick: jest.fn(),
commandButtonLabel: "Test Button 2",
ariaLabel: "Test Button 2 Aria Label",
tooltipText: "Test Tooltip 2",
hasPopup: false,
disabled: false,
},
];
const mockFluentItems = [
{
key: "button1",
text: "Test Button 1",
iconProps: { iconName: "Add" },
},
{
key: "button2",
text: "Test Button 2",
iconProps: { iconName: "Feedback" },
},
];
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
mockConvertButton.mockReturnValue(mockFluentItems);
const { container } = render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockConvertButton).toHaveBeenCalledTimes(1);
expect(container.querySelector(".commandBarContainer")).toBeInTheDocument();
});
it("should handle multiple command bar buttons", () => {
const mockCommandButtonProps: CommandButtonComponentProps[] = [
{
iconSrc: "create.svg",
iconAlt: "Create",
onCommandClick: jest.fn(),
commandButtonLabel: "Create Copy Job",
ariaLabel: "Create Copy Job",
tooltipText: "Create Copy Job",
hasPopup: false,
disabled: false,
},
{
iconSrc: "refresh.svg",
iconAlt: "Refresh",
onCommandClick: jest.fn(),
commandButtonLabel: "Refresh",
ariaLabel: "Refresh",
tooltipText: "Refresh",
hasPopup: false,
disabled: false,
},
{
iconSrc: "feedback.svg",
iconAlt: "Feedback",
onCommandClick: jest.fn(),
commandButtonLabel: "Feedback",
ariaLabel: "Feedback",
tooltipText: "Feedback",
hasPopup: false,
disabled: false,
},
];
mockGetCommandBarButtons.mockReturnValue(mockCommandButtonProps);
mockConvertButton.mockReturnValue([
{ key: "create", text: "Create Copy Job" },
{ key: "refresh", text: "Refresh" },
{ key: "feedback", text: "Feedback" },
]);
render(<CopyJobCommandBar explorer={mockExplorer} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer, false);
expect(mockConvertButton.mock.calls[0][0]).toEqual(mockCommandButtonProps);
});
it("should re-render when explorer prop changes", () => {
const mockExplorer1 = { id: "explorer1" } as unknown as Explorer;
const mockExplorer2 = { id: "explorer2" } as unknown as Explorer;
mockGetCommandBarButtons.mockReturnValue([]);
mockConvertButton.mockReturnValue([]);
const { rerender } = render(<CopyJobCommandBar explorer={mockExplorer1} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer1, false);
rerender(<CopyJobCommandBar explorer={mockExplorer2} />);
expect(mockGetCommandBarButtons).toHaveBeenCalledWith(mockExplorer2, false);
expect(mockGetCommandBarButtons).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,37 +0,0 @@
import { CommandBar as FluentCommandBar, ICommandBarItemProps } from "@fluentui/react";
import React from "react";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import * as CommandBarUtil from "../../Menus/CommandBar/CommandBarUtil";
import { getThemeTokens } from "../../Theme/ThemeUtil";
import { ContainerCopyProps } from "../Types/CopyJobTypes";
import { getCommandBarButtons } from "./Utils";
const CopyJobCommandBar: React.FC<ContainerCopyProps> = ({ explorer }) => {
const isDarkMode = useThemeStore((state) => state.isDarkMode);
const themeTokens = getThemeTokens(isDarkMode);
const backgroundColor = themeTokens.colorNeutralBackground1;
const rootStyle = {
root: {
backgroundColor: backgroundColor,
},
};
const commandBarItems: CommandButtonComponentProps[] = getCommandBarButtons(explorer, isDarkMode);
const controlButtons: ICommandBarItemProps[] = CommandBarUtil.convertButton(commandBarItems, backgroundColor);
return (
<div className="commandBarContainer" style={{ backgroundColor }}>
<FluentCommandBar
ariaLabel="Use left and right arrow keys to navigate between commands"
styles={rootStyle}
items={controlButtons}
/>
</div>
);
};
CopyJobCommandBar.displayName = "CopyJobCommandBar";
export default CopyJobCommandBar;

View File

@@ -1,272 +0,0 @@
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { getCommandBarButtons } from "./Utils";
jest.mock("../../../ConfigContext", () => ({
configContext: {
platform: "Portal",
},
Platform: {
Portal: "Portal",
Emulator: "Emulator",
Hosted: "Hosted",
},
}));
jest.mock("../Actions/CopyJobActions", () => ({
openCreateCopyJobPanel: jest.fn(),
}));
jest.mock("../MonitorCopyJobs/MonitorCopyJobRefState", () => ({
MonitorCopyJobsRefState: jest.fn(),
}));
describe("CommandBar Utils", () => {
let mockExplorer: Explorer;
let mockOpenContainerCopyFeedbackBlade: jest.Mock;
let mockRefreshJobList: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockOpenContainerCopyFeedbackBlade = jest.fn();
mockRefreshJobList = jest.fn();
mockExplorer = {
openContainerCopyFeedbackBlade: mockOpenContainerCopyFeedbackBlade,
} as unknown as Explorer;
(MonitorCopyJobsRefState as unknown as jest.Mock).mockImplementation((selector) => {
const state = {
ref: {
refreshJobList: mockRefreshJobList,
},
};
return selector(state);
});
});
describe("getCommandBarButtons", () => {
it("should return an array of command button props", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons).toBeDefined();
expect(Array.isArray(buttons)).toBe(true);
expect(buttons.length).toBeGreaterThan(0);
});
it("should include create copy job button", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
const createButton = buttons[0];
expect(createButton).toBeDefined();
expect(createButton.commandButtonLabel).toBeUndefined();
expect(createButton.ariaLabel).toBe("Create a new container copy job");
expect(createButton.tooltipText).toBe("Create Copy Job");
expect(createButton.hasPopup).toBe(false);
expect(createButton.disabled).toBe(false);
});
it("should include refresh button", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1];
expect(refreshButton).toBeDefined();
expect(refreshButton.ariaLabel).toBe("Refresh copy jobs");
expect(refreshButton.tooltipText).toBe("Refresh");
expect(refreshButton.disabled).toBe(false);
});
it("should include feedback button when platform is Portal", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons.length).toBe(4);
const feedbackButton = buttons[3];
expect(feedbackButton).toBeDefined();
expect(feedbackButton.ariaLabel).toBe("Provide feedback on copy jobs");
expect(feedbackButton.tooltipText).toBe("Feedback");
expect(feedbackButton.disabled).toBe(false);
});
it("should not include feedback button when platform is not Portal", async () => {
jest.resetModules();
jest.doMock("../../../ConfigContext", () => ({
configContext: {
platform: "Emulator",
},
Platform: {
Portal: "Portal",
Emulator: "Emulator",
Hosted: "Hosted",
},
}));
const { getCommandBarButtons: getCommandBarButtonsEmulator } = await import("./Utils");
const buttons = getCommandBarButtonsEmulator(mockExplorer, false);
expect(buttons.length).toBe(3);
});
it("should call openCreateCopyJobPanel when create button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
const createButton = buttons[0];
createButton.onCommandClick({} as React.SyntheticEvent);
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledTimes(1);
});
it("should call refreshJobList when refresh button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1];
refreshButton.onCommandClick({} as React.SyntheticEvent);
expect(mockRefreshJobList).toHaveBeenCalledTimes(1);
});
it("should call openContainerCopyFeedbackBlade when feedback button is clicked", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
const feedbackButton = buttons[3];
feedbackButton.onCommandClick({} as React.SyntheticEvent);
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalledTimes(1);
});
it("should return buttons with correct icon sources", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons[0].iconSrc).toBeDefined();
expect(buttons[0].iconAlt).toBe("Create Copy Job");
expect(buttons[1].iconSrc).toBeDefined();
expect(buttons[1].iconAlt).toBe("Refresh");
expect(buttons[2].iconSrc).toBeDefined();
expect(buttons[2].iconAlt).toBe("Dark Theme");
expect(buttons[3].iconSrc).toBeDefined();
expect(buttons[3].iconAlt).toBe("Feedback");
});
it("should handle null MonitorCopyJobsRefState ref gracefully", () => {
(MonitorCopyJobsRefState as unknown as jest.Mock).mockImplementationOnce((selector) => {
const state: { ref: null } = { ref: null };
return selector(state);
});
const buttons = getCommandBarButtons(mockExplorer, false);
const refreshButton = buttons[1];
expect(() => refreshButton.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
});
it("should set hasPopup to false for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.hasPopup).toBe(false);
});
});
it("should set commandButtonLabel to undefined for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.commandButtonLabel).toBeUndefined();
});
});
it("should respect disabled state when provided", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.disabled).toBe(false);
});
});
it("should return CommandButtonComponentProps with all required properties", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button: CommandButtonComponentProps) => {
expect(button).toHaveProperty("iconSrc");
expect(button).toHaveProperty("iconAlt");
expect(button).toHaveProperty("onCommandClick");
expect(button).toHaveProperty("commandButtonLabel");
expect(button).toHaveProperty("ariaLabel");
expect(button).toHaveProperty("tooltipText");
expect(button).toHaveProperty("hasPopup");
expect(button).toHaveProperty("disabled");
});
});
it("should maintain button order: create, refresh, themeToggle, feedback", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
expect(buttons[0].tooltipText).toBe("Create Copy Job");
expect(buttons[1].tooltipText).toBe("Refresh");
expect(buttons[2].tooltipText).toBe("Dark Theme");
expect(buttons[3].tooltipText).toBe("Feedback");
});
});
describe("Button click handlers", () => {
it("should execute click handlers without errors", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(() => button.onCommandClick({} as React.SyntheticEvent)).not.toThrow();
});
});
it("should call correct action for each button", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons[0].onCommandClick({} as React.SyntheticEvent);
expect(Actions.openCreateCopyJobPanel).toHaveBeenCalledWith(mockExplorer);
buttons[1].onCommandClick({} as React.SyntheticEvent);
expect(mockRefreshJobList).toHaveBeenCalled();
buttons[3].onCommandClick({} as React.SyntheticEvent);
expect(mockOpenContainerCopyFeedbackBlade).toHaveBeenCalled();
});
});
describe("Accessibility", () => {
it("should have aria labels for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.ariaLabel).toBeDefined();
expect(typeof button.ariaLabel).toBe("string");
expect(button.ariaLabel.length).toBeGreaterThan(0);
});
});
it("should have tooltip text for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.tooltipText).toBeDefined();
expect(typeof button.tooltipText).toBe("string");
expect(button.tooltipText.length).toBeGreaterThan(0);
});
});
it("should have icon alt text for all buttons", () => {
const buttons = getCommandBarButtons(mockExplorer, false);
buttons.forEach((button) => {
expect(button.iconAlt).toBeDefined();
expect(typeof button.iconAlt).toBe("string");
expect(button.iconAlt.length).toBeGreaterThan(0);
});
});
});
});

View File

@@ -1,70 +0,0 @@
import AddIcon from "../../../../images/Add.svg";
import FeedbackIcon from "../../../../images/Feedback-Command.svg";
import MoonIcon from "../../../../images/MoonIcon.svg";
import RefreshIcon from "../../../../images/refresh-cosmos.svg";
import SunIcon from "../../../../images/SunIcon.svg";
import { configContext, Platform } from "../../../ConfigContext";
import { useThemeStore } from "../../../hooks/useTheme";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../../Explorer";
import * as Actions from "../Actions/CopyJobActions";
import ContainerCopyMessages from "../ContainerCopyMessages";
import { MonitorCopyJobsRefState } from "../MonitorCopyJobs/MonitorCopyJobRefState";
import { CopyJobCommandBarBtnType } from "../Types/CopyJobTypes";
function getCopyJobBtns(explorer: Explorer, isDarkMode: boolean): CopyJobCommandBarBtnType[] {
const monitorCopyJobsRef = MonitorCopyJobsRefState((state) => state.ref);
const buttons: CopyJobCommandBarBtnType[] = [
{
key: "createCopyJob",
iconSrc: AddIcon,
label: ContainerCopyMessages.createCopyJobButtonLabel,
ariaLabel: ContainerCopyMessages.createCopyJobButtonAriaLabel,
onClick: () => Actions.openCreateCopyJobPanel(explorer),
},
{
key: "refresh",
iconSrc: RefreshIcon,
label: ContainerCopyMessages.refreshButtonLabel,
ariaLabel: ContainerCopyMessages.refreshButtonAriaLabel,
onClick: () => monitorCopyJobsRef?.refreshJobList(),
},
{
key: "themeToggle",
iconSrc: isDarkMode ? SunIcon : MoonIcon,
label: isDarkMode ? "Light Theme" : "Dark Theme",
ariaLabel: isDarkMode ? "Switch to Light Theme" : "Switch to Dark Theme",
onClick: () => useThemeStore.getState().toggleTheme(),
},
];
if (configContext.platform === Platform.Portal) {
buttons.push({
key: "feedback",
iconSrc: FeedbackIcon,
label: ContainerCopyMessages.feedbackButtonLabel,
ariaLabel: ContainerCopyMessages.feedbackButtonAriaLabel,
onClick: () => {
explorer.openContainerCopyFeedbackBlade();
},
});
}
return buttons;
}
function btnMapper(config: CopyJobCommandBarBtnType): CommandButtonComponentProps {
return {
iconSrc: config.iconSrc,
iconAlt: config.label,
onCommandClick: config.onClick,
commandButtonLabel: undefined as string | undefined,
ariaLabel: config.ariaLabel,
tooltipText: config.label,
hasPopup: false,
disabled: config.disabled ?? false,
};
}
export function getCommandBarButtons(explorer: Explorer, isDarkMode: boolean): CommandButtonComponentProps[] {
return getCopyJobBtns(explorer, isDarkMode).map(btnMapper);
}

View File

@@ -1,188 +0,0 @@
export default {
// Copy Job Command Bar
feedbackButtonLabel: "Feedback",
feedbackButtonAriaLabel: "Provide feedback on copy jobs",
refreshButtonLabel: "Refresh",
refreshButtonAriaLabel: "Refresh copy jobs",
createCopyJobButtonLabel: "Create Copy Job",
createCopyJobButtonAriaLabel: "Create a new container copy job",
// No Copy Jobs Found
noCopyJobsTitle: "No copy jobs to show",
createCopyJobButtonText: "Create a container copy job",
// Copy Job Details
copyJobDetailsPanelTitle: (jobName: string) => jobName || "Job Details",
errorTitle: "Error Details",
selectedContainers: "Selected Containers",
// Create Copy Job Panel
createCopyJobPanelTitle: "Create copy job",
// Select Account Screen
selectAccountDescription: "Please select a source account from which to copy.",
subscriptionDropdownLabel: "Subscription",
subscriptionDropdownPlaceholder: "Select a subscription",
sourceAccountDropdownLabel: "Account",
sourceAccountDropdownPlaceholder: "Select an account",
migrationTypeOptions: {
offline: {
title: "Offline mode",
description:
"Offline container copy jobs let you copy data from a source container to a destination Cosmos DB container for supported APIs. To ensure data integrity between the source and destination, we recommend stopping updates on the source container before creating the copy job. Learn more about [offline copy jobs](https://learn.microsoft.com/azure/cosmos-db/how-to-container-copy?tabs=offline-copy&pivots=api-nosql).",
},
online: {
title: "Online mode",
description:
"Online container copy jobs let you copy data from a source container to a destination Cosmos DB NoSQL API container using the [All Versions and Delete](https://learn.microsoft.com/azure/cosmos-db/change-feed-modes?tabs=all-versions-and-deletes#all-versions-and-deletes-change-feed-mode-preview) change feed. This allows updates to continue on the source while data is copied. A brief downtime is required at the end to safely switch over client applications to the destination container. Learn more about [online copy jobs](https://learn.microsoft.com/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#getting-started).",
},
},
// Select Source and Target Containers Screen
selectSourceAndTargetContainersDescription:
"Please select a source container and a destination container to copy to.",
sourceContainerSubHeading: "Source container",
targetContainerSubHeading: "Destination container",
databaseDropdownLabel: "Database",
databaseDropdownPlaceholder: "Select a database",
containerDropdownLabel: "Container",
containerDropdownPlaceholder: "Select a container",
createNewContainerSubHeading: "Select the properties for your container.",
createContainerButtonLabel: "Create a new container",
createContainerHeading: "Create new container",
// Preview and Create Screen
jobNameLabel: "Job name",
sourceSubscriptionLabel: "Source subscription",
sourceAccountLabel: "Source account",
sourceDatabaseLabel: "Source database",
sourceContainerLabel: "Source container",
targetDatabaseLabel: "Destination database",
targetContainerLabel: "Destination container",
// Assign Permissions Screen
assignPermissions: {
crossAccountDescription:
"To copy data from the source to the destination container, ensure that the managed identity of the destination account has read access to the source account by completing the following steps.",
intraAccountOnlineDescription: (accountName: string) =>
`Follow the steps below to enable online copy on your "${accountName}" account.`,
crossAccountConfiguration: {
title: "Cross-account container copy",
description: (sourceAccount: string, destinationAccount: string) =>
`Please follow the instruction below to grant requisite permissions to copy data from "${sourceAccount}" to "${destinationAccount}".`,
},
onlineConfiguration: {
title: "Online container copy",
description: (accountName: string) =>
`Please follow the instructions below to enable online copy on your "${accountName}" account.`,
},
},
toggleBtn: {
onText: "On",
offText: "Off",
},
popoverOverlaySpinnerLabel: "Please wait while we process your request...",
addManagedIdentity: {
title: "System-assigned managed identity enabled.",
description:
"A system-assigned managed identity is restricted to one per resource and is tied to the lifecycle of this resource. Once enabled, you can grant permissions to the managed identity by using Azure role-based access control (Azure RBAC). The managed identity is authenticated with Microsoft Entra ID, so you dont have to store any credentials in code.",
descriptionHrefText: "Learn more about Managed identities.",
descriptionHref: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
toggleLabel: "System assigned managed identity",
tooltip: {
content: "Learn more about",
hrefText: "Managed Identities.",
href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
},
userAssignedIdentityTooltip: "You can select an existing user assigned identity or create a new one.",
userAssignedIdentityLabel: "You may also select a user assigned managed identity.",
createUserAssignedIdentityLink: "Create User Assigned Managed Identity",
enablementTitle: "Enable system assigned managed identity",
enablementDescription: (accountName: string) =>
accountName
? `Enable system-assigned managed identity on the ${accountName}. To confirm, click the "Yes" button.`
: "",
},
defaultManagedIdentity: {
title: "System-assigned managed identity set as default.",
description: (accountName: string) =>
`Set the system-assigned managed identity as default for "${accountName}" by switching it on.`,
tooltip: {
content: "Learn more about",
hrefText: "Default Managed Identities.",
href: "https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/overview",
},
popoverTitle: "System assigned managed identity set as default",
popoverDescription: (accountName: string) =>
`Assign the system-assigned managed identity as the default for "${accountName}". To confirm, click the "Yes" button. `,
},
readPermissionAssigned: {
title: "Read permissions assigned to the default identity.",
description:
"To allow data copy from source to the destination container, provide read access of the source account to the default identity of the destination account.",
tooltip: {
content: "Learn more about",
hrefText: "Read permissions.",
href: "https://learn.microsoft.com/azure/cosmos-db/nosql/how-to-connect-role-based-access-control",
},
popoverTitle: "Read permissions assigned to default identity.",
popoverDescription:
"Assign read permissions of the source account to the default identity of the destination account. To confirm click the “Yes” button.",
},
pointInTimeRestore: {
title: "Point In Time Restore enabled",
description: (accessName: string) =>
`To facilitate online container copy jobs, please update your "${accessName}" backup policy from periodic to continuous backup. Enabling continuous backup is required for this functionality.`,
tooltip: {
content: "Learn more about",
hrefText: "Continuous Backup",
href: "https://learn.microsoft.com/en-us/azure/cosmos-db/continuous-backup-restore-introduction",
},
buttonText: "Enable Point In Time Restore",
},
onlineCopyEnabled: {
title: "Online copy enabled",
description: (accountName: string) =>
`Enable online container copy by clicking the button below on your "${accountName}" account.`,
hrefText: "Learn more about online copy jobs",
href: "https://learn.microsoft.com/en-us/azure/cosmos-db/container-copy?tabs=online-copy&pivots=api-nosql#enable-online-copy",
buttonText: "Enable Online Copy",
validateAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Validating All versions and deletes change feed mode (preview)...",
enablingAllVersionsAndDeletesChangeFeedSpinnerLabel:
"Enabling All versions and deletes change feed mode (preview)...",
enablingOnlineCopySpinnerLabel: (accountName: string) =>
`Enabling online copy on your "${accountName}" account ...`,
},
MonitorJobs: {
Columns: {
lastUpdatedTime: "Date & time",
name: "Job name",
status: "Status",
completionPercentage: "Completion %",
duration: "Duration",
error: "Error message",
mode: "Mode",
actions: "Actions",
},
Actions: {
pause: "Pause",
resume: "Resume",
cancel: "Cancel",
complete: "Complete",
viewDetails: "View Details",
},
Status: {
Pending: "Queued",
InProgress: "Running",
Running: "Running",
Partitioning: "Running",
Paused: "Paused",
Completed: "Completed",
Failed: "Failed",
Faulted: "Failed",
Skipped: "Cancelled",
Cancelled: "Cancelled",
},
},
};

View File

@@ -1,131 +0,0 @@
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import React from "react";
import Explorer from "../Explorer";
import ContainerCopyPanel from "./ContainerCopyPanel";
import { MonitorCopyJobsRefState } from "./MonitorCopyJobs/MonitorCopyJobRefState";
jest.mock("./CommandBar/CopyJobCommandBar", () => {
const MockCopyJobCommandBar = () => {
return <div data-testid="copy-job-command-bar">CopyJobCommandBar</div>;
};
MockCopyJobCommandBar.displayName = "CopyJobCommandBar";
return MockCopyJobCommandBar;
});
jest.mock("./MonitorCopyJobs/MonitorCopyJobs", () => {
const React = jest.requireActual("react");
const MockMonitorCopyJobs = React.forwardRef((_props: any, ref: any) => {
React.useImperativeHandle(ref, () => ({
refreshJobList: jest.fn(),
}));
return <div data-testid="monitor-copy-jobs">MonitorCopyJobs</div>;
});
MockMonitorCopyJobs.displayName = "MonitorCopyJobs";
return MockMonitorCopyJobs;
});
jest.mock("./MonitorCopyJobs/MonitorCopyJobRefState", () => ({
MonitorCopyJobsRefState: {
getState: jest.fn(() => ({
setRef: jest.fn(),
})),
},
}));
describe("ContainerCopyPanel", () => {
let mockExplorer: Explorer;
let mockSetRef: jest.Mock;
beforeEach(() => {
mockExplorer = {} as Explorer;
mockSetRef = jest.fn();
(MonitorCopyJobsRefState.getState as jest.Mock).mockReturnValue({
setRef: mockSetRef,
});
});
afterEach(() => {
jest.clearAllMocks();
});
it("renders the component with correct structure", () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
const wrapper = document.querySelector("#containerCopyWrapper");
expect(wrapper).toBeInTheDocument();
expect(wrapper).toHaveClass("flexContainer", "hideOverflows");
});
it("renders CopyJobCommandBar component", () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
const commandBar = screen.getByTestId("copy-job-command-bar");
expect(commandBar).toBeInTheDocument();
expect(commandBar).toHaveTextContent("CopyJobCommandBar");
});
it("renders MonitorCopyJobs component", () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
const monitorCopyJobs = screen.getByTestId("monitor-copy-jobs");
expect(monitorCopyJobs).toBeInTheDocument();
expect(monitorCopyJobs).toHaveTextContent("MonitorCopyJobs");
});
it("passes explorer prop to child components", () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
expect(screen.getByTestId("copy-job-command-bar")).toBeInTheDocument();
expect(screen.getByTestId("monitor-copy-jobs")).toBeInTheDocument();
});
it("sets the MonitorCopyJobs ref in the state on mount", async () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
await waitFor(() => {
expect(mockSetRef).toHaveBeenCalledTimes(1);
});
const refArgument = mockSetRef.mock.calls[0][0];
expect(refArgument).toBeDefined();
expect(refArgument).toHaveProperty("refreshJobList");
expect(typeof refArgument.refreshJobList).toBe("function");
});
it("updates the ref state when monitorCopyJobsRef changes", async () => {
const { rerender } = render(<ContainerCopyPanel explorer={mockExplorer} />);
await waitFor(() => {
expect(mockSetRef).toHaveBeenCalledTimes(1);
});
mockSetRef.mockClear();
rerender(<ContainerCopyPanel explorer={mockExplorer} />);
});
it("handles missing explorer prop gracefully", () => {
const { container } = render(<ContainerCopyPanel explorer={undefined as any} />);
expect(container.querySelector("#containerCopyWrapper")).toBeInTheDocument();
});
it("applies correct CSS classes to wrapper", () => {
render(<ContainerCopyPanel explorer={mockExplorer} />);
const wrapper = document.querySelector("#containerCopyWrapper");
expect(wrapper).toHaveClass("flexContainer");
expect(wrapper).toHaveClass("hideOverflows");
});
it("maintains ref across re-renders", async () => {
const { rerender } = render(<ContainerCopyPanel explorer={mockExplorer} />);
await waitFor(() => {
expect(mockSetRef).toHaveBeenCalled();
});
const firstCallRef = mockSetRef.mock.calls[0][0];
const newExplorer = {} as Explorer;
rerender(<ContainerCopyPanel explorer={newExplorer} />);
expect(mockSetRef.mock.calls[0][0]).toBe(firstCallRef);
});
});

View File

@@ -1,25 +0,0 @@
import React, { useEffect } from "react";
import CopyJobCommandBar from "./CommandBar/CopyJobCommandBar";
import "./containerCopyStyles.less";
import { MonitorCopyJobsRefState } from "./MonitorCopyJobs/MonitorCopyJobRefState";
import MonitorCopyJobs, { MonitorCopyJobsRef } from "./MonitorCopyJobs/MonitorCopyJobs";
import { ContainerCopyProps } from "./Types/CopyJobTypes";
const ContainerCopyPanel: React.FC<ContainerCopyProps> = ({ explorer }) => {
const monitorCopyJobsRef = React.useRef<MonitorCopyJobsRef>();
useEffect(() => {
if (monitorCopyJobsRef.current) {
MonitorCopyJobsRefState.getState().setRef(monitorCopyJobsRef.current);
}
}, [monitorCopyJobsRef.current]);
return (
<div id="containerCopyWrapper" className="flexContainer hideOverflows">
<CopyJobCommandBar explorer={explorer} />
<MonitorCopyJobs ref={monitorCopyJobsRef} explorer={explorer} />
</div>
);
};
ContainerCopyPanel.displayName = "ContainerCopyPanel";
export default ContainerCopyPanel;

View File

@@ -1,660 +0,0 @@
import "@testing-library/jest-dom";
import { act, render, screen } from "@testing-library/react";
import React from "react";
import Explorer from "../../Explorer";
import { CopyJobMigrationType } from "../Enums/CopyJobEnums";
import CopyJobContextProvider, { CopyJobContext, useCopyJobContext } from "./CopyJobContext";
jest.mock("UserContext", () => ({
userContext: {
subscriptionId: "test-subscription-id",
databaseAccount: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
kind: "GlobalDocumentDB",
},
},
}));
describe("CopyJobContext", () => {
let mockExplorer: Explorer;
beforeEach(() => {
mockExplorer = {} as Explorer;
});
afterEach(() => {
jest.clearAllMocks();
});
describe("CopyJobContextProvider", () => {
it("should render children correctly", () => {
render(
<CopyJobContextProvider explorer={mockExplorer}>
<div data-testid="test-child">Test Child</div>
</CopyJobContextProvider>,
);
expect(screen.getByTestId("test-child")).toBeInTheDocument();
expect(screen.getByTestId("test-child")).toHaveTextContent("Test Child");
});
it("should initialize with default state", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue).toBeDefined();
expect(contextValue.copyJobState).toEqual({
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: "test-subscription-id",
account: {
id: "/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-account",
name: "test-account",
location: "East US",
kind: "GlobalDocumentDB",
},
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
});
expect(contextValue.flow).toBeNull();
expect(contextValue.contextError).toBeNull();
expect(contextValue.explorer).toBe(mockExplorer);
});
it("should provide setCopyJobState function", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.setCopyJobState).toBeDefined();
expect(typeof contextValue.setCopyJobState).toBe("function");
});
it("should provide setFlow function", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.setFlow).toBeDefined();
expect(typeof contextValue.setFlow).toBe("function");
});
it("should provide setContextError function", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.setContextError).toBeDefined();
expect(typeof contextValue.setContextError).toBe("function");
});
it("should provide resetCopyJobState function", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.resetCopyJobState).toBeDefined();
expect(typeof contextValue.resetCopyJobState).toBe("function");
});
it("should update copyJobState when setCopyJobState is called", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
return (
<button
onClick={() =>
context.setCopyJobState({
...context.copyJobState,
jobName: "test-job",
migrationType: CopyJobMigrationType.Online,
})
}
>
Update Job
</button>
);
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
const button = screen.getByText("Update Job");
act(() => {
button.click();
});
expect(contextValue.copyJobState.jobName).toBe("test-job");
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Online);
});
it("should update flow when setFlow is called", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
const handleSetFlow = (): void => {
context.setFlow({ currentScreen: "source-selection" });
};
return <button onClick={handleSetFlow}>Set Flow</button>;
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
expect(contextValue.flow).toBeNull();
const button = screen.getByText("Set Flow");
act(() => {
button.click();
});
expect(contextValue.flow).toEqual({ currentScreen: "source-selection" });
});
it("should update contextError when setContextError is called", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
return <button onClick={() => context.setContextError("Test error message")}>Set Error</button>;
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
expect(contextValue.contextError).toBeNull();
const button = screen.getByText("Set Error");
act(() => {
button.click();
});
expect(contextValue.contextError).toBe("Test error message");
});
it("should reset copyJobState when resetCopyJobState is called", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
const handleUpdate = (): void => {
context.setCopyJobState({
...context.copyJobState,
jobName: "modified-job",
migrationType: CopyJobMigrationType.Online,
source: {
...context.copyJobState.source,
databaseId: "test-db",
containerId: "test-container",
},
});
};
return (
<>
<button onClick={handleUpdate}>Update</button>
<button onClick={context.resetCopyJobState}>Reset</button>
</>
);
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
const updateButton = screen.getByText("Update");
act(() => {
updateButton.click();
});
expect(contextValue.copyJobState.jobName).toBe("modified-job");
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Online);
expect(contextValue.copyJobState.source.databaseId).toBe("test-db");
const resetButton = screen.getByText("Reset");
act(() => {
resetButton.click();
});
expect(contextValue.copyJobState.jobName).toBe("");
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Offline);
expect(contextValue.copyJobState.source.databaseId).toBe("");
expect(contextValue.copyJobState.source.containerId).toBe("");
});
it("should maintain explorer reference", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.explorer).toBe(mockExplorer);
});
it("should handle multiple state updates correctly", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
return (
<>
<button onClick={() => context.setCopyJobState({ ...context.copyJobState, jobName: "job-1" })}>
Update 1
</button>
<button onClick={() => context.setFlow({ currentScreen: "screen-1" })}>Flow 1</button>
<button onClick={() => context.setContextError("error-1")}>Error 1</button>
</>
);
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
act(() => {
screen.getByText("Update 1").click();
});
expect(contextValue.copyJobState.jobName).toBe("job-1");
act(() => {
screen.getByText("Flow 1").click();
});
expect(contextValue.flow).toEqual({ currentScreen: "screen-1" });
act(() => {
screen.getByText("Error 1").click();
});
expect(contextValue.contextError).toBe("error-1");
});
it("should handle partial state updates", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
const handlePartialUpdate = (): void => {
context.setCopyJobState((prev) => ({
...prev,
jobName: "partial-update",
}));
};
return <button onClick={handlePartialUpdate}>Partial Update</button>;
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
const initialState = { ...contextValue.copyJobState };
act(() => {
screen.getByText("Partial Update").click();
});
expect(contextValue.copyJobState.jobName).toBe("partial-update");
expect(contextValue.copyJobState.migrationType).toBe(initialState.migrationType);
expect(contextValue.copyJobState.source).toEqual(initialState.source);
expect(contextValue.copyJobState.target).toEqual(initialState.target);
});
});
describe("useCopyJobContext", () => {
it("should return context value when used within provider", () => {
let contextValue: any;
const TestComponent = (): null => {
const context = useCopyJobContext();
contextValue = context;
return null;
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
expect(contextValue).toBeDefined();
expect(contextValue.copyJobState).toBeDefined();
expect(contextValue.setCopyJobState).toBeDefined();
expect(contextValue.flow).toBeNull();
expect(contextValue.setFlow).toBeDefined();
expect(contextValue.contextError).toBeNull();
expect(contextValue.setContextError).toBeDefined();
expect(contextValue.resetCopyJobState).toBeDefined();
expect(contextValue.explorer).toBe(mockExplorer);
});
it("should throw error when used outside provider", () => {
const originalError = console.error;
console.error = jest.fn();
const TestComponent = (): null => {
useCopyJobContext();
return null;
};
expect(() => {
render(<TestComponent />);
}).toThrow("useCopyJobContext must be used within a CopyJobContextProvider");
console.error = originalError;
});
it("should allow updating state through hook", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
return (
<button
onClick={() =>
context.setCopyJobState({
...context.copyJobState,
jobName: "hook-test-job",
})
}
>
Update
</button>
);
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
act(() => {
screen.getByText("Update").click();
});
expect(contextValue.copyJobState.jobName).toBe("hook-test-job");
});
it("should allow resetting state through hook", () => {
let contextValue: any;
const TestComponent = (): JSX.Element => {
const context = useCopyJobContext();
contextValue = context;
return (
<>
<button
onClick={() =>
context.setCopyJobState({
...context.copyJobState,
jobName: "modified",
source: {
...context.copyJobState.source,
databaseId: "modified-db",
},
})
}
>
Modify
</button>
<button onClick={() => context.resetCopyJobState()}>Reset</button>
</>
);
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent />
</CopyJobContextProvider>,
);
act(() => {
screen.getByText("Modify").click();
});
expect(contextValue.copyJobState.jobName).toBe("modified");
expect(contextValue.copyJobState.source.databaseId).toBe("modified-db");
act(() => {
screen.getByText("Reset").click();
});
expect(contextValue.copyJobState.jobName).toBe("");
expect(contextValue.copyJobState.source.databaseId).toBe("");
});
it("should maintain state consistency across multiple components", () => {
let contextValue1: any;
let contextValue2: any;
const TestComponent1 = (): JSX.Element => {
const context = useCopyJobContext();
contextValue1 = context;
return (
<button
onClick={() =>
context.setCopyJobState({
...context.copyJobState,
jobName: "shared-job",
})
}
>
Update From Component 1
</button>
);
};
const TestComponent2 = (): JSX.Element => {
const context = useCopyJobContext();
contextValue2 = context;
return <div data-testid="component-2">Component 2</div>;
};
render(
<CopyJobContextProvider explorer={mockExplorer}>
<TestComponent1 />
<TestComponent2 />
</CopyJobContextProvider>,
);
expect(contextValue1.copyJobState).toEqual(contextValue2.copyJobState);
act(() => {
screen.getByText("Update From Component 1").click();
});
expect(contextValue1.copyJobState.jobName).toBe("shared-job");
expect(contextValue2.copyJobState.jobName).toBe("shared-job");
});
});
describe("Initial State", () => {
it("should initialize with offline migration type", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.migrationType).toBe(CopyJobMigrationType.Offline);
});
it("should initialize source with userContext values", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.source?.subscription?.subscriptionId).toBeUndefined();
expect(contextValue.copyJobState.source?.account?.name).toBeUndefined();
});
it("should initialize target with userContext values", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.target.subscriptionId).toBe("test-subscription-id");
expect(contextValue.copyJobState.target.account.name).toBe("test-account");
});
it("should initialize sourceReadAccessFromTarget as false", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.sourceReadAccessFromTarget).toBe(false);
});
it("should initialize with empty database and container ids", () => {
let contextValue: any;
render(
<CopyJobContextProvider explorer={mockExplorer}>
<CopyJobContext.Consumer>
{(value) => {
contextValue = value;
return null;
}}
</CopyJobContext.Consumer>
</CopyJobContextProvider>,
);
expect(contextValue.copyJobState.source.databaseId).toBe("");
expect(contextValue.copyJobState.source.containerId).toBe("");
expect(contextValue.copyJobState.target.databaseId).toBe("");
expect(contextValue.copyJobState.target.containerId).toBe("");
});
});
});

View File

@@ -1,64 +0,0 @@
import Explorer from "Explorer/Explorer";
import React from "react";
import { userContext } from "UserContext";
import { CopyJobMigrationType } from "../Enums/CopyJobEnums";
import { CopyJobContextProviderType, CopyJobContextState, CopyJobFlowType } from "../Types/CopyJobTypes";
export const CopyJobContext = React.createContext<CopyJobContextProviderType>(null);
export const useCopyJobContext = (): CopyJobContextProviderType => {
const context = React.useContext(CopyJobContext);
if (!context) {
throw new Error("useCopyJobContext must be used within a CopyJobContextProvider");
}
return context;
};
interface CopyJobContextProviderProps {
children: React.ReactNode;
explorer: Explorer;
}
const getInitialCopyJobState = (): CopyJobContextState => {
return {
jobName: "",
migrationType: CopyJobMigrationType.Offline,
source: {
subscription: null,
account: null,
databaseId: "",
containerId: "",
},
target: {
subscriptionId: userContext.subscriptionId || "",
account: userContext.databaseAccount || null,
databaseId: "",
containerId: "",
},
sourceReadAccessFromTarget: false,
};
};
const CopyJobContextProvider: React.FC<CopyJobContextProviderProps> = (props) => {
const [copyJobState, setCopyJobState] = React.useState<CopyJobContextState>(getInitialCopyJobState());
const [flow, setFlow] = React.useState<CopyJobFlowType | null>(null);
const [contextError, setContextError] = React.useState<string | null>(null);
const resetCopyJobState = () => {
setCopyJobState(getInitialCopyJobState());
};
const contextValue: CopyJobContextProviderType = {
contextError,
setContextError,
copyJobState,
setCopyJobState,
flow,
setFlow,
resetCopyJobState,
explorer: props.explorer,
};
return <CopyJobContext.Provider value={contextValue}>{props.children}</CopyJobContext.Provider>;
};
export default CopyJobContextProvider;

View File

@@ -1,490 +0,0 @@
import { DatabaseAccount } from "Contracts/DataModels";
import * as CopyJobUtils from "./CopyJobUtils";
import { CopyJobContextState, CopyJobErrorType, CopyJobType } from "./Types/CopyJobTypes";
describe("CopyJobUtils", () => {
describe("buildResourceLink", () => {
const mockResource: DatabaseAccount = {
id: "/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
name: "account1",
location: "eastus",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {},
};
let originalLocation: Location;
beforeEach(() => {
originalLocation = window.location;
});
afterEach(() => {
(window as any).location = originalLocation;
});
it("should build resource link with Azure portal endpoint", () => {
delete (window as any).location;
(window as any).location = {
...originalLocation,
origin: "https://portal.azure.com",
ancestorOrigins: ["https://portal.azure.com"] as any,
} as Location;
const link = CopyJobUtils.buildResourceLink(mockResource);
expect(link).toBe(
"https://portal.azure.com/#resource/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
);
});
it("should replace cosmos.azure with portal.azure", () => {
delete (window as any).location;
(window as any).location = {
...originalLocation,
origin: "https://cosmos.azure.com",
ancestorOrigins: ["https://cosmos.azure.com"] as any,
} as Location;
const link = CopyJobUtils.buildResourceLink(mockResource);
expect(link).toContain("https://portal.azure.com");
});
it("should use Azure portal endpoint for localhost", () => {
delete (window as any).location;
(window as any).location = {
...originalLocation,
origin: "http://localhost:1234",
ancestorOrigins: ["http://localhost:1234"] as any,
} as Location;
const link = CopyJobUtils.buildResourceLink(mockResource);
expect(link).toContain("https://ms.portal.azure.com");
});
it("should remove trailing slash from origin", () => {
delete (window as any).location;
(window as any).location = {
...originalLocation,
origin: "https://portal.azure.com/",
ancestorOrigins: ["https://portal.azure.com/"] as any,
} as Location;
const link = CopyJobUtils.buildResourceLink(mockResource);
expect(link).toBe(
"https://portal.azure.com/#resource/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
);
});
});
describe("buildDataTransferJobPath", () => {
it("should build basic path without jobName or action", () => {
const path = CopyJobUtils.buildDataTransferJobPath({
subscriptionId: "sub123",
resourceGroup: "rg1",
accountName: "account1",
});
expect(path).toBe(
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs",
);
});
it("should build path with jobName", () => {
const path = CopyJobUtils.buildDataTransferJobPath({
subscriptionId: "sub123",
resourceGroup: "rg1",
accountName: "account1",
jobName: "job1",
});
expect(path).toBe(
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs/job1",
);
});
it("should build path with jobName and action", () => {
const path = CopyJobUtils.buildDataTransferJobPath({
subscriptionId: "sub123",
resourceGroup: "rg1",
accountName: "account1",
jobName: "job1",
action: "cancel",
});
expect(path).toBe(
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1/dataTransferJobs/job1/cancel",
);
});
});
describe("convertTime", () => {
it("should convert time string with hours, minutes, and seconds", () => {
const result = CopyJobUtils.convertTime("02:30:45");
expect(result).toBe("02 hours, 30 minutes, 45 seconds");
});
it("should convert time string with only seconds", () => {
const result = CopyJobUtils.convertTime("00:00:30");
expect(result).toBe("30 seconds");
});
it("should convert time string with only minutes and seconds", () => {
const result = CopyJobUtils.convertTime("00:05:15");
expect(result).toBe("05 minutes, 15 seconds");
});
it("should round seconds", () => {
const result = CopyJobUtils.convertTime("00:00:45.678");
expect(result).toBe("46 seconds");
});
it("should return '0 seconds' for zero time", () => {
const result = CopyJobUtils.convertTime("00:00:00");
expect(result).toBe("0 seconds");
});
it("should return null for invalid time format", () => {
const result = CopyJobUtils.convertTime("invalid");
expect(result).toBeNull();
});
it("should return null for incomplete time string", () => {
const result = CopyJobUtils.convertTime("10:30");
expect(result).toBeNull();
});
it("should pad single digit values", () => {
const result = CopyJobUtils.convertTime("1:5:9");
expect(result).toBe("01 hours, 05 minutes, 09 seconds");
});
});
describe("formatUTCDateTime", () => {
it("should format valid UTC date string", () => {
const result = CopyJobUtils.formatUTCDateTime("2025-11-26T10:30:00Z");
expect(result).not.toBeNull();
expect(result?.formattedDateTime).toContain("11/26/25, 10:30:00 AM");
expect(result?.timestamp).toBeGreaterThan(0);
});
it("should return null for invalid date string", () => {
const result = CopyJobUtils.formatUTCDateTime("invalid-date");
expect(result).toBeNull();
});
it("should return timestamp for valid date", () => {
const result = CopyJobUtils.formatUTCDateTime("2025-01-01T00:00:00Z");
expect(result).not.toBeNull();
expect(typeof result?.timestamp).toBe("number");
expect(result?.timestamp).toBe(new Date("2025-01-01T00:00:00Z").getTime());
});
});
describe("convertToCamelCase", () => {
it("should convert string to camel case", () => {
const result = CopyJobUtils.convertToCamelCase("hello world");
expect(result).toBe("HelloWorld");
});
it("should handle single word", () => {
const result = CopyJobUtils.convertToCamelCase("hello");
expect(result).toBe("Hello");
});
it("should handle multiple spaces", () => {
const result = CopyJobUtils.convertToCamelCase("hello world test");
expect(result).toBe("HelloWorldTest");
});
it("should handle mixed case input", () => {
const result = CopyJobUtils.convertToCamelCase("HELLO WORLD");
expect(result).toBe("HelloWorld");
});
it("should handle empty string", () => {
const result = CopyJobUtils.convertToCamelCase("");
expect(result).toBe("");
});
});
describe("extractErrorMessage", () => {
it("should extract first part of error message before line breaks", () => {
const error: CopyJobErrorType = {
message: "Error occurred\r\n\r\nAdditional details\r\n\r\nMore info",
code: "500",
};
const result = CopyJobUtils.extractErrorMessage(error);
expect(result.message).toBe("Error occurred");
expect(result.code).toBe("500");
});
it("should return same message if no line breaks", () => {
const error: CopyJobErrorType = {
message: "Simple error message",
code: "404",
};
const result = CopyJobUtils.extractErrorMessage(error);
expect(result.message).toBe("Simple error message");
expect(result.code).toBe("404");
});
});
describe("getAccountDetailsFromResourceId", () => {
it("should extract account details from valid resource ID", () => {
const resourceId =
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
const details = CopyJobUtils.getAccountDetailsFromResourceId(resourceId);
expect(details).toEqual({
subscriptionId: "sub123",
resourceGroup: "rg1",
accountName: "account1",
});
});
it("should be case insensitive", () => {
const resourceId =
"/subscriptions/sub123/resourceGroups/rg1/providers/microsoft.documentdb/databaseAccounts/account1";
const details = CopyJobUtils.getAccountDetailsFromResourceId(resourceId);
expect(details).toEqual({
subscriptionId: "sub123",
resourceGroup: "rg1",
accountName: "account1",
});
});
it("should return null for undefined resource ID", () => {
const details = CopyJobUtils.getAccountDetailsFromResourceId(undefined);
expect(details).toBeNull();
});
it("should return null for invalid resource ID", () => {
const details = CopyJobUtils.getAccountDetailsFromResourceId("invalid-resource-id");
expect(details).toEqual({ accountName: undefined, resourceGroup: undefined, subscriptionId: undefined });
});
});
describe("getContainerIdentifiers", () => {
it("should extract container identifiers", () => {
const container = {
account: {
id: "/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1",
name: "account1",
location: "eastus",
type: "Microsoft.DocumentDB/databaseAccounts",
kind: "GlobalDocumentDB",
properties: {},
},
databaseId: "db1",
containerId: "container1",
} as CopyJobContextState["source"];
const identifiers = CopyJobUtils.getContainerIdentifiers(container);
expect(identifiers).toEqual({
accountId: container.account.id,
databaseId: "db1",
containerId: "container1",
});
});
it("should return empty strings for undefined values", () => {
const container = {
account: undefined,
databaseId: undefined,
containerId: undefined,
} as CopyJobContextState["source"];
const identifiers = CopyJobUtils.getContainerIdentifiers(container);
expect(identifiers).toEqual({
accountId: "",
databaseId: "",
containerId: "",
});
});
});
describe("isIntraAccountCopy", () => {
const sourceAccountId =
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
const targetAccountId =
"/subscriptions/sub123/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
const differentAccountId =
"/subscriptions/sub456/resourceGroups/rg2/providers/Microsoft.DocumentDB/databaseAccounts/account2";
it("should return true for same account", () => {
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, targetAccountId);
expect(result).toBe(true);
});
it("should return false for different accounts", () => {
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentAccountId);
expect(result).toBe(false);
});
it("should return false for different subscriptions", () => {
const differentSubId =
"/subscriptions/sub999/resourceGroups/rg1/providers/Microsoft.DocumentDB/databaseAccounts/account1";
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentSubId);
expect(result).toBe(false);
});
it("should return false for different resource groups", () => {
const differentRgId =
"/subscriptions/sub123/resourceGroups/rg999/providers/Microsoft.DocumentDB/databaseAccounts/account1";
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, differentRgId);
expect(result).toBe(false);
});
it("should return false for undefined source", () => {
const result = CopyJobUtils.isIntraAccountCopy(undefined, targetAccountId);
expect(result).toBe(false);
});
it("should return false for undefined target", () => {
const result = CopyJobUtils.isIntraAccountCopy(sourceAccountId, undefined);
expect(result).toBe(false);
});
});
describe("isEqual", () => {
const createMockJob = (name: string, status: string): CopyJobType => ({
ID: name,
Mode: "Online",
Name: name,
Status: status as any,
CompletionPercentage: 50,
Duration: "00:05:00",
LastUpdatedTime: "2025-11-26T10:00:00Z",
timestamp: Date.now(),
Source: {} as any,
Destination: {} as any,
});
it("should return true for equal job arrays", () => {
const jobs1 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
const jobs2 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(true);
});
it("should return false for different lengths", () => {
const jobs1 = [createMockJob("job1", "Running")];
const jobs2 = [createMockJob("job1", "Running"), createMockJob("job2", "Completed")];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(false);
});
it("should return false for different status", () => {
const jobs1 = [createMockJob("job1", "Running")];
const jobs2 = [createMockJob("job1", "Completed")];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(false);
});
it("should return false for missing job in second array", () => {
const jobs1 = [createMockJob("job1", "Running")];
const jobs2 = [createMockJob("job2", "Running")];
const result = CopyJobUtils.isEqual(jobs1, jobs2);
expect(result).toBe(false);
});
it("should return true for empty arrays", () => {
const result = CopyJobUtils.isEqual([], []);
expect(result).toBe(true);
});
});
describe("getDefaultJobName", () => {
beforeEach(() => {
jest.spyOn(Date.prototype, "getTime").mockReturnValue(1234567890);
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should generate default job name for single container", () => {
const containers = [
{
sourceDatabaseName: "sourceDb",
sourceContainerName: "sourceCont",
targetDatabaseName: "targetDb",
targetContainerName: "targetCont",
},
];
const jobName = CopyJobUtils.getDefaultJobName(containers);
expect(jobName).toBe("sourc.sourc_targe.targe_1234567890");
});
it("should truncate long names", () => {
const containers = [
{
sourceDatabaseName: "veryLongSourceDatabaseName",
sourceContainerName: "veryLongSourceContainerName",
targetDatabaseName: "veryLongTargetDatabaseName",
targetContainerName: "veryLongTargetContainerName",
},
];
const jobName = CopyJobUtils.getDefaultJobName(containers);
expect(jobName).toBe("veryL.veryL_veryL.veryL_1234567890");
});
it("should return empty string for multiple containers", () => {
const containers = [
{
sourceDatabaseName: "db1",
sourceContainerName: "cont1",
targetDatabaseName: "db2",
targetContainerName: "cont2",
},
{
sourceDatabaseName: "db3",
sourceContainerName: "cont3",
targetDatabaseName: "db4",
targetContainerName: "cont4",
},
];
const jobName = CopyJobUtils.getDefaultJobName(containers);
expect(jobName).toBe("");
});
it("should return empty string for empty array", () => {
const jobName = CopyJobUtils.getDefaultJobName([]);
expect(jobName).toBe("");
});
it("should handle short names without truncation", () => {
const containers = [
{
sourceDatabaseName: "src",
sourceContainerName: "cont",
targetDatabaseName: "tgt",
targetContainerName: "dest",
},
];
const jobName = CopyJobUtils.getDefaultJobName(containers);
expect(jobName).toBe("src.cont_tgt.dest_1234567890");
});
});
describe("constants", () => {
it("should have correct COSMOS_SQL_COMPONENT value", () => {
expect(CopyJobUtils.COSMOS_SQL_COMPONENT).toBe("CosmosDBSql");
});
it("should have correct COPY_JOB_API_VERSION value", () => {
expect(CopyJobUtils.COPY_JOB_API_VERSION).toBe("2025-05-01-preview");
});
});
});

View File

@@ -1,171 +0,0 @@
import { DatabaseAccount } from "Contracts/DataModels";
import { CopyJobContextState, CopyJobErrorType, CopyJobType } from "./Types/CopyJobTypes";
const azurePortalMpacEndpoint = "https://ms.portal.azure.com/";
export const buildResourceLink = (resource: DatabaseAccount): string => {
const resourceId = resource.id;
let parentOrigin = window.location.ancestorOrigins?.[0] ?? window.location.origin;
if (/\/\/localhost:/.test(parentOrigin)) {
parentOrigin = azurePortalMpacEndpoint;
} else if (/\/\/cosmos\.azure/.test(parentOrigin)) {
parentOrigin = parentOrigin.replace("cosmos.azure", "portal.azure");
}
parentOrigin = parentOrigin.replace(/\/$/, "");
return `${parentOrigin}/#resource${resourceId}`;
};
export const COSMOS_SQL_COMPONENT = "CosmosDBSql";
export const COPY_JOB_API_VERSION = "2025-05-01-preview";
export function buildDataTransferJobPath({
subscriptionId,
resourceGroup,
accountName,
jobName,
action,
}: {
subscriptionId: string;
resourceGroup: string;
accountName: string;
jobName?: string;
action?: string;
}) {
let path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/dataTransferJobs`;
if (jobName) {
path += `/${jobName}`;
}
if (action) {
path += `/${action}`;
}
return path;
}
export function convertTime(timeStr: string): string | null {
const timeParts = timeStr.split(":").map(Number);
if (timeParts.length !== 3 || timeParts.some(isNaN)) {
return null;
}
const formatPart = (value: number, unit: string) => {
if (unit === "seconds") {
value = Math.round(value);
}
return value > 0 ? `${value.toString().padStart(2, "0")} ${unit}` : "";
};
const [hours, minutes, seconds] = timeParts;
const formattedTimeParts = [
formatPart(hours, "hours"),
formatPart(minutes, "minutes"),
formatPart(seconds, "seconds"),
]
.filter(Boolean)
.join(", ");
return formattedTimeParts || "0 seconds";
}
export function formatUTCDateTime(utcStr: string): { formattedDateTime: string; timestamp: number } | null {
const date = new Date(utcStr);
if (isNaN(date.getTime())) {
return null;
}
return {
formattedDateTime: new Intl.DateTimeFormat("en-US", {
dateStyle: "short",
timeStyle: "medium",
timeZone: "UTC",
}).format(date),
timestamp: date.getTime(),
};
}
export function convertToCamelCase(str: string): string {
const formattedStr = str
.split(/\s+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("");
return formattedStr;
}
export function extractErrorMessage(error: CopyJobErrorType): CopyJobErrorType {
return {
...error,
message: error.message.split("\r\n\r\n")[0],
};
}
export function getAccountDetailsFromResourceId(accountId: string | undefined) {
if (!accountId) {
return null;
}
const pattern = new RegExp(
"/subscriptions/([^/]+)/resourceGroups/([^/]+)/providers/Microsoft\\.DocumentDB?/databaseAccounts/([^/]+)",
"i",
);
const matches = accountId.match(pattern);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, subscriptionId, resourceGroup, accountName] = matches || [];
return { subscriptionId, resourceGroup, accountName };
}
export function getContainerIdentifiers(container: CopyJobContextState["source"] | CopyJobContextState["target"]) {
return {
accountId: container?.account?.id || "",
databaseId: container?.databaseId || "",
containerId: container?.containerId || "",
};
}
export function isIntraAccountCopy(sourceAccountId: string | undefined, targetAccountId: string | undefined): boolean {
const sourceAccountDetails = getAccountDetailsFromResourceId(sourceAccountId);
const targetAccountDetails = getAccountDetailsFromResourceId(targetAccountId);
return (
sourceAccountDetails?.subscriptionId === targetAccountDetails?.subscriptionId &&
sourceAccountDetails?.resourceGroup === targetAccountDetails?.resourceGroup &&
sourceAccountDetails?.accountName === targetAccountDetails?.accountName
);
}
export function isEqual(prevJobs: CopyJobType[], newJobs: CopyJobType[]): boolean {
if (prevJobs.length !== newJobs.length) {
return false;
}
return prevJobs.every((prevJob: CopyJobType) => {
const newJob = newJobs.find((job) => job.Name === prevJob.Name);
if (!newJob) {
return false;
}
return prevJob.Status === newJob.Status;
});
}
const truncateLength = 5;
export const truncateName = (name: string, length: number = truncateLength): string => {
return name.length <= length ? name : name.slice(0, length);
};
export function getDefaultJobName(
selectedDatabaseAndContainers: {
sourceDatabaseName?: string;
sourceContainerName?: string;
targetDatabaseName?: string;
targetContainerName?: string;
}[],
): string {
if (selectedDatabaseAndContainers.length === 1) {
const { sourceDatabaseName, sourceContainerName, targetDatabaseName, targetContainerName } =
selectedDatabaseAndContainers[0];
const timestamp = new Date().getTime().toString();
const sourcePart = `${truncateName(sourceDatabaseName)}.${truncateName(sourceContainerName)}`;
const targetPart = `${truncateName(targetDatabaseName)}.${truncateName(targetContainerName)}`;
return `${sourcePart}_${targetPart}_${timestamp}`;
}
return "";
}

Some files were not shown because too many files have changed in this diff Show More