mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
70 Commits
postgres_q
...
copilot_co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66a00c67ca | ||
|
|
8eccbd2806 | ||
|
|
4617fa9364 | ||
|
|
a282ad9242 | ||
|
|
b954b14f56 | ||
|
|
ed9c7dbb6b | ||
|
|
aadbb50e7d | ||
|
|
abff435e88 | ||
|
|
d1c4059320 | ||
|
|
9b214a3171 | ||
|
|
ba66aa939b | ||
|
|
f9af595eee | ||
|
|
7ce8c7a5a1 | ||
|
|
9ad3f589cc | ||
|
|
9267b2cc18 | ||
|
|
c7d1b2dcdb | ||
|
|
a526410e44 | ||
|
|
58cb85156c | ||
|
|
dd5ccade2b | ||
|
|
38ebef6c02 | ||
|
|
7ad5862e8f | ||
|
|
755b732532 | ||
|
|
1b5a9b83ff | ||
|
|
fb8871cfbf | ||
|
|
874cec26fc | ||
|
|
9d2d0e4754 | ||
|
|
c7c894d6d9 | ||
|
|
547954c3dc | ||
|
|
7f220bf8be | ||
|
|
1ee6abf890 | ||
|
|
72c3605dbe | ||
|
|
a7a38807df | ||
|
|
1285ffc440 | ||
|
|
b4bca3d41a | ||
|
|
2a47e4311c | ||
|
|
49d9f489d8 | ||
|
|
31cc129aa7 | ||
|
|
99af4acca4 | ||
|
|
80dd161a6f | ||
|
|
850f1dfb97 | ||
|
|
a827e79317 | ||
|
|
7dbccff41d | ||
|
|
9184684e75 | ||
|
|
701f486d8f | ||
|
|
5059917edf | ||
|
|
ab1409efb1 | ||
|
|
5de9e682ba | ||
|
|
b2ab979360 | ||
|
|
2f32a676d0 | ||
|
|
950c8ee470 | ||
|
|
b0eaac5b84 | ||
|
|
952491a3ad | ||
|
|
5dde66b032 | ||
|
|
5b1db2778c | ||
|
|
1213788f9c | ||
|
|
1ce3adff0f | ||
|
|
00eb07da11 | ||
|
|
afe59c1589 | ||
|
|
53b5ebd39c | ||
|
|
5b365e642f | ||
|
|
333b3de587 | ||
|
|
e909ac43f4 | ||
|
|
8433a027ad | ||
|
|
81dfd76198 | ||
|
|
7c77ffda6c | ||
|
|
a34d3bb000 | ||
|
|
42731d1aae | ||
|
|
3abbb63adc | ||
|
|
2e618cb3c4 | ||
|
|
beca0d6608 |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -92,11 +92,11 @@ jobs:
|
||||
name: dist
|
||||
path: dist/
|
||||
- name: Upload build to preview blob storage
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload-batch -d '$web' -s 'dist' --account-name cosmosexplorerpreview --destination-path "${{github.event.pull_request.head.sha || github.sha}}" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
- name: Upload preview config to blob storage
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}"
|
||||
run: az storage blob upload -c '$web' -f ./preview/config.json --account-name cosmosexplorerpreview --name "${{github.event.pull_request.head.sha || github.sha}}/config.json" --account-key="${PREVIEW_STORAGE_KEY}" --overwrite true
|
||||
env:
|
||||
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
|
||||
endtoendemulator:
|
||||
@@ -182,7 +182,7 @@ jobs:
|
||||
with:
|
||||
name: dist
|
||||
- run: cp ./configs/prod.json config.json
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
@@ -207,7 +207,7 @@ jobs:
|
||||
name: dist
|
||||
- run: cp ./configs/mpac.json config.json
|
||||
- run: sed -i 's/Azure.Cosmos.DB.Data.Explorer/Azure.Cosmos.DB.Data.Explorer.MPAC/g' DataExplorer.nuspec
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "jawelton@microsoft.com" -Password "$AZURE_DEVOPS_PAT"
|
||||
- run: nuget pack -Version "2.0.0-github-${GITHUB_SHA}"
|
||||
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
|
||||
- uses: actions/upload-artifact@v2
|
||||
|
||||
41
SECURITY.md
Normal file
41
SECURITY.md
Normal file
@@ -0,0 +1,41 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.7 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html class="default no-js">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@@ -9,256 +10,292 @@
|
||||
<link rel="stylesheet" href="../assets/css/main.css">
|
||||
<script async src="../assets/js/search.js" id="search-script"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="tsd-page-toolbar">
|
||||
<div class="container">
|
||||
<div class="table-wrap">
|
||||
<div class="table-cell" id="tsd-search" data-index="../assets/js/search.json" data-base="..">
|
||||
<div class="field">
|
||||
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
|
||||
<input id="tsd-search-field" type="text" />
|
||||
</div>
|
||||
<ul class="results">
|
||||
<li class="state loading">Preparing search index...</li>
|
||||
<li class="state failure">The search index is not available</li>
|
||||
</ul>
|
||||
<a href="../index.html" class="title">cosmos-explorer</a>
|
||||
</div>
|
||||
<div class="table-cell" id="tsd-widgets">
|
||||
<div id="tsd-filter">
|
||||
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
|
||||
<div class="tsd-filter-group">
|
||||
<div class="tsd-select" id="tsd-filter-visibility">
|
||||
<span class="tsd-select-label">All</span>
|
||||
<ul class="tsd-select-list">
|
||||
<li data-value="public">Public</li>
|
||||
<li data-value="protected">Public/Protected</li>
|
||||
<li data-value="private" class="selected">All</li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="checkbox" id="tsd-filter-inherited" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
|
||||
<input type="checkbox" id="tsd-filter-externals" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
|
||||
<header>
|
||||
<div class="tsd-page-toolbar">
|
||||
<div class="container">
|
||||
<div class="table-wrap">
|
||||
<div class="table-cell" id="tsd-search" data-index="../assets/js/search.json" data-base="..">
|
||||
<div class="field">
|
||||
<label for="tsd-search-field" class="tsd-widget search no-caption">Search</label>
|
||||
<input id="tsd-search-field" type="text" />
|
||||
</div>
|
||||
<ul class="results">
|
||||
<li class="state loading">Preparing search index...</li>
|
||||
<li class="state failure">The search index is not available</li>
|
||||
</ul>
|
||||
<a href="../index.html" class="title">cosmos-explorer</a>
|
||||
</div>
|
||||
<div class="table-cell" id="tsd-widgets">
|
||||
<div id="tsd-filter">
|
||||
<a href="#" class="tsd-widget options no-caption" data-toggle="options">Options</a>
|
||||
<div class="tsd-filter-group">
|
||||
<div class="tsd-select" id="tsd-filter-visibility">
|
||||
<span class="tsd-select-label">All</span>
|
||||
<ul class="tsd-select-list">
|
||||
<li data-value="public">Public</li>
|
||||
<li data-value="protected">Public/Protected</li>
|
||||
<li data-value="private" class="selected">All</li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="checkbox" id="tsd-filter-inherited" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-inherited">Inherited</label>
|
||||
<input type="checkbox" id="tsd-filter-externals" checked />
|
||||
<label class="tsd-widget" for="tsd-filter-externals">Externals</label>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
|
||||
</div>
|
||||
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu">Menu</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tsd-page-title">
|
||||
<div class="container">
|
||||
<ul class="tsd-breadcrumb">
|
||||
<li>
|
||||
<a href="../modules.html">cosmos-explorer</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../modules/selfserve_selfserveutils.html">SelfServe/SelfServeUtils</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="selfserve_selfserveutils.bladetype.html">BladeType</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h1>Enumeration BladeType</h1>
|
||||
<div class="tsd-page-title">
|
||||
<div class="container">
|
||||
<ul class="tsd-breadcrumb">
|
||||
<li>
|
||||
<a href="../modules.html">cosmos-explorer</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="../modules/selfserve_selfserveutils.html">SelfServe/SelfServeUtils</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="selfserve_selfserveutils.bladetype.html">BladeType</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h1>Enumeration BladeType</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container container-main">
|
||||
<div class="row">
|
||||
<div class="col-8 col-content">
|
||||
<section class="tsd-panel tsd-comment">
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Portal Blade types</p>
|
||||
</header>
|
||||
<div class="container container-main">
|
||||
<div class="row">
|
||||
<div class="col-8 col-content">
|
||||
<section class="tsd-panel tsd-comment">
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Portal Blade types</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel-group tsd-index-group">
|
||||
<h2>Index</h2>
|
||||
<section class="tsd-panel tsd-index-panel">
|
||||
<div class="tsd-index-content">
|
||||
<section class="tsd-index-section ">
|
||||
<h3>Enumeration members</h3>
|
||||
<ul class="tsd-index-list">
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#cassandrakeys" class="tsd-kind-icon">Cassandra<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#gremlinkeys" class="tsd-kind-icon">Gremlin<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#metrics" class="tsd-kind-icon">Metrics</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#mongokeys" class="tsd-kind-icon">Mongo<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#sqlkeys" class="tsd-kind-icon">Sql<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a href="selfserve_selfserveutils.bladetype.html#tablekeys" class="tsd-kind-icon">Table<wbr>Keys</a></li>
|
||||
</section>
|
||||
<section class="tsd-panel-group tsd-index-group">
|
||||
<h2>Index</h2>
|
||||
<section class="tsd-panel tsd-index-panel">
|
||||
<div class="tsd-index-content">
|
||||
<section class="tsd-index-section ">
|
||||
<h3>Enumeration members</h3>
|
||||
<ul class="tsd-index-list">
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#cassandrakeys"
|
||||
class="tsd-kind-icon">Cassandra<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#gremlinkeys"
|
||||
class="tsd-kind-icon">Gremlin<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#metrics"
|
||||
class="tsd-kind-icon">Metrics</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#mongokeys"
|
||||
class="tsd-kind-icon">Mongo<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#sqlkeys"
|
||||
class="tsd-kind-icon">Sql<wbr>Keys</a></li>
|
||||
<li class="tsd-kind-enum-member tsd-parent-kind-enum"><a
|
||||
href="selfserve_selfserveutils.bladetype.html#tablekeys"
|
||||
class="tsd-kind-icon">Table<wbr>Keys</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section class="tsd-panel-group tsd-member-group ">
|
||||
<h2>Enumeration members</h2>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="cassandrakeys" class="tsd-anchor"></a>
|
||||
<h3>Cassandra<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Cassandra<wbr>Keys<span
|
||||
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
|
||||
"cassandraDbKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Azure Cosmos DB for Apache Cassandra account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="gremlinkeys" class="tsd-anchor"></a>
|
||||
<h3>Gremlin<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Gremlin<wbr>Keys<span
|
||||
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
|
||||
"keys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Azure Cosmos DB for Apache Gremlin account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="metrics" class="tsd-anchor"></a>
|
||||
<h3>Metrics</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Metrics<span class="tsd-signature-symbol">:</span>
|
||||
<span class="tsd-signature-symbol"> = "metrics"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Metrics blade of an Azure Cosmos DB account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="mongokeys" class="tsd-anchor"></a>
|
||||
<h3>Mongo<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Mongo<wbr>Keys<span
|
||||
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
|
||||
"mongoDbKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Azure Cosmos DB for MongoDB account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="sqlkeys" class="tsd-anchor"></a>
|
||||
<h3>Sql<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Sql<wbr>Keys<span class="tsd-signature-symbol">:</span>
|
||||
<span class="tsd-signature-symbol"> = "keys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Azure Cosmos DB for NoSQL account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="tablekeys" class="tsd-anchor"></a>
|
||||
<h3>Table<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Table<wbr>Keys<span
|
||||
class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> =
|
||||
"tableKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Azure Cosmos DB for Table account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
|
||||
<nav class="tsd-navigation primary">
|
||||
<ul>
|
||||
<li class=" ">
|
||||
<a href="../modules.html">Modules</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve.html">Self<wbr>Serve</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve___what_is_currently_supported_.html">Self<wbr>Serve -<wbr>
|
||||
<wbr>What is currently supported?</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve_decorators.html">Self<wbr>Serve/<wbr>Decorators</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a
|
||||
href="../modules/selfserve_selfservetelemetryprocessor.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Telemetry<wbr>Processor</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a
|
||||
href="../modules/selfserve_selfservetypes.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Types</a>
|
||||
</li>
|
||||
<li class="current tsd-kind-module">
|
||||
<a
|
||||
href="../modules/selfserve_selfserveutils.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Utils</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="tsd-navigation secondary menu-sticky">
|
||||
<ul class="before-current">
|
||||
</ul>
|
||||
<ul class="current">
|
||||
<li class="current tsd-kind-enum tsd-parent-kind-module">
|
||||
<a href="selfserve_selfserveutils.bladetype.html" class="tsd-kind-icon">Blade<wbr>Type</a>
|
||||
<ul>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#cassandrakeys"
|
||||
class="tsd-kind-icon">Cassandra<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#gremlinkeys"
|
||||
class="tsd-kind-icon">Gremlin<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#metrics"
|
||||
class="tsd-kind-icon">Metrics</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#mongokeys"
|
||||
class="tsd-kind-icon">Mongo<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#sqlkeys"
|
||||
class="tsd-kind-icon">Sql<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#tablekeys"
|
||||
class="tsd-kind-icon">Table<wbr>Keys</a>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
<section class="tsd-panel-group tsd-member-group ">
|
||||
<h2>Enumeration members</h2>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="cassandrakeys" class="tsd-anchor"></a>
|
||||
<h3>Cassandra<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Cassandra<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "cassandraDbKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Cassandra API account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="gremlinkeys" class="tsd-anchor"></a>
|
||||
<h3>Gremlin<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Gremlin<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "keys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Gremlin API account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="metrics" class="tsd-anchor"></a>
|
||||
<h3>Metrics</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Metrics<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "metrics"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Metrics blade of an Azure Cosmos DB account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="mongokeys" class="tsd-anchor"></a>
|
||||
<h3>Mongo<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Mongo<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "mongoDbKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Mongo API account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="sqlkeys" class="tsd-anchor"></a>
|
||||
<h3>Sql<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Sql<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "keys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a SQL API account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="tsd-panel tsd-member tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a name="tablekeys" class="tsd-anchor"></a>
|
||||
<h3>Table<wbr>Keys</h3>
|
||||
<div class="tsd-signature tsd-kind-icon">Table<wbr>Keys<span class="tsd-signature-symbol">:</span> <span class="tsd-signature-symbol"> = "tableKeys"</span></div>
|
||||
<aside class="tsd-sources">
|
||||
</aside>
|
||||
<div class="tsd-comment tsd-typography">
|
||||
<div class="lead">
|
||||
<p>Keys blade of a Table API account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-4 col-menu menu-sticky-wrap menu-highlight">
|
||||
<nav class="tsd-navigation primary">
|
||||
<ul>
|
||||
<li class=" ">
|
||||
<a href="../modules.html">Modules</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve.html">Self<wbr>Serve</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve___what_is_currently_supported_.html">Self<wbr>Serve -<wbr> <wbr>What is currently supported?</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve_decorators.html">Self<wbr>Serve/<wbr>Decorators</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve_selfservetelemetryprocessor.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Telemetry<wbr>Processor</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-module">
|
||||
<a href="../modules/selfserve_selfservetypes.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Types</a>
|
||||
</li>
|
||||
<li class="current tsd-kind-module">
|
||||
<a href="../modules/selfserve_selfserveutils.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Utils</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<nav class="tsd-navigation secondary menu-sticky">
|
||||
<ul class="before-current">
|
||||
</ul>
|
||||
<ul class="current">
|
||||
<li class="current tsd-kind-enum tsd-parent-kind-module">
|
||||
<a href="selfserve_selfserveutils.bladetype.html" class="tsd-kind-icon">Blade<wbr>Type</a>
|
||||
<ul>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#cassandrakeys" class="tsd-kind-icon">Cassandra<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#gremlinkeys" class="tsd-kind-icon">Gremlin<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#metrics" class="tsd-kind-icon">Metrics</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#mongokeys" class="tsd-kind-icon">Mongo<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#sqlkeys" class="tsd-kind-icon">Sql<wbr>Keys</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-enum-member tsd-parent-kind-enum">
|
||||
<a href="selfserve_selfserveutils.bladetype.html#tablekeys" class="tsd-kind-icon">Table<wbr>Keys</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="after-current">
|
||||
<li class=" tsd-kind-enum tsd-parent-kind-module">
|
||||
<a href="selfserve_selfserveutils.selfservetype.html" class="tsd-kind-icon">Self<wbr>Serve<wbr>Type</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-function tsd-parent-kind-module">
|
||||
<a href="../modules/selfserve_selfserveutils.html#generatebladelink" class="tsd-kind-icon">generate<wbr>Blade<wbr>Link</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="after-current">
|
||||
<li class=" tsd-kind-enum tsd-parent-kind-module">
|
||||
<a href="selfserve_selfserveutils.selfservetype.html"
|
||||
class="tsd-kind-icon">Self<wbr>Serve<wbr>Type</a>
|
||||
</li>
|
||||
<li class=" tsd-kind-function tsd-parent-kind-module">
|
||||
<a href="../modules/selfserve_selfserveutils.html#generatebladelink"
|
||||
class="tsd-kind-icon">generate<wbr>Blade<wbr>Link</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="with-border-bottom">
|
||||
<div class="container">
|
||||
<h2>Legend</h2>
|
||||
<div class="tsd-legend-group">
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li>
|
||||
<li class="tsd-kind-type-alias"><span class="tsd-kind-icon">Type alias</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li>
|
||||
</ul>
|
||||
<footer class="with-border-bottom">
|
||||
<div class="container">
|
||||
<h2>Legend</h2>
|
||||
<div class="tsd-legend-group">
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-function"><span class="tsd-kind-icon">Function</span></li>
|
||||
<li class="tsd-kind-type-alias"><span class="tsd-kind-icon">Type alias</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-enum"><span class="tsd-kind-icon">Enumeration</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-interface"><span class="tsd-kind-icon">Interface</span></li>
|
||||
</ul>
|
||||
<ul class="tsd-legend">
|
||||
<li class="tsd-kind-class"><span class="tsd-kind-icon">Class</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="container tsd-generator">
|
||||
<p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="container tsd-generator">
|
||||
<p>Generated using <a href="https://typedoc.org/" target="_blank">TypeDoc</a></p>
|
||||
</div>
|
||||
<div class="overlay"></div>
|
||||
<script src="../assets/js/main.js"></script>
|
||||
<div class="overlay"></div>
|
||||
<script src="../assets/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
35
images/Copilot.svg
Normal file
35
images/Copilot.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="40" height="40" fill="white"/>
|
||||
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint0_radial_420_101763)"/>
|
||||
<path d="M17 3.73926L20 4.04436V12.6862L15.2 13.8013L13.6 15.9967V19.4479C13.6 21.7669 14.8545 23.9045 16.879 25.0353L21.4036 27.5626L13.6 31.69L10.3559 32.1523L7.27902 30.4337C5.25445 29.3028 4 27.1652 4 24.8462V14.7591C4 12.4395 5.25512 10.3015 7.28055 9.17085L16.3174 4.12639L16.3121 4.13012L17 3.73926Z" fill="url(#paint1_linear_420_101763)"/>
|
||||
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint2_radial_420_101763)"/>
|
||||
<path d="M26.399 15.001L34.399 19.801L35.999 21.401V24.8452C35.999 27.1642 34.7446 29.3018 32.72 30.4327L23.12 35.7949C21.1804 36.8784 18.8177 36.8784 16.878 35.7949L7.27804 30.4327C7.09146 30.3284 6.91141 30.2157 6.73828 30.095L7.278 30.3965C9.21762 31.4799 11.5803 31.4799 13.52 30.3965L23.12 25.0342C25.1445 23.9033 26.399 21.7657 26.399 19.4467V15.001Z" fill="url(#paint3_linear_420_101763)"/>
|
||||
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint4_radial_420_101763)"/>
|
||||
<path d="M32.7191 9.17053L23.119 3.8117C21.1802 2.72943 18.819 2.72943 16.8802 3.8117L16.3151 4.1271C14.6239 5.31737 13.5996 7.26472 13.5996 9.36025V16.0461L16.8802 14.2149C18.819 13.1326 21.1802 13.1326 23.119 14.2149L32.7191 19.5737C34.6984 20.6786 35.9421 22.7456 35.9977 25.0039C35.999 24.9514 35.9996 24.8987 35.9996 24.8459V14.7588C35.9996 12.4392 34.7445 10.3011 32.7191 9.17053Z" fill="url(#paint5_linear_420_101763)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(17.5054 12.8837) rotate(112.544) scale(23.4519 19.3067)">
|
||||
<stop offset="0.206732" stop-color="#45D586"/>
|
||||
<stop offset="0.875628" stop-color="#128245"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint1_linear_420_101763" x1="15.0625" y1="29.6174" x2="13.787" y2="27.2436" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0F773F"/>
|
||||
<stop offset="1" stop-color="#0078D4" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint2_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(10.899 27.5214) rotate(-3.9995) scale(24.6197 15.4594)">
|
||||
<stop offset="0.140029" stop-color="#FBFF47"/>
|
||||
<stop offset="0.952721" stop-color="#54B228"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_420_101763" x1="30.8932" y1="18.5508" x2="29.5491" y2="20.8921" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#27770B"/>
|
||||
<stop offset="1" stop-color="#8C66BA" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_420_101763" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(30.632 19.3878) rotate(-138.33) scale(22.8015 13.0047)">
|
||||
<stop stop-color="#95FEA0"/>
|
||||
<stop offset="0.839255" stop-color="#10B7B7"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint5_linear_420_101763" x1="13.5996" y1="11.7134" x2="16.2131" y2="11.7134" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.9999" stop-color="#0A7B7B"/>
|
||||
<stop offset="1" stop-color="#436DCD" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
images/YoutubePlaceholder.svg
Normal file
3
images/YoutubePlaceholder.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="334" height="154" viewBox="0 0 334 154" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="333" height="153" fill="#F3F2F1" stroke="#E1DFDD"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 188 B |
BIN
images/firewallRule.png
Normal file
BIN
images/firewallRule.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" ?>
|
||||
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||
<svg enable-background="new 0 0 256 256" height="256px" id="Layer_1" version="1.1" viewBox="0 0 256 256" width="256px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path stroke="white" stroke-width="0.5" fill="#b5a3a3" d="M179.199,38.399c0,1.637-0.625,3.274-1.875,4.524l-85.076,85.075l85.076,85.075c2.5,2.5,2.5,6.55,0,9.05s-6.55,2.5-9.05,0 l-89.601-89.6c-2.5-2.5-2.5-6.551,0-9.051l89.601-89.6c2.5-2.5,6.55-2.5,9.05,0C178.574,35.124,179.199,36.762,179.199,38.399z"/>
|
||||
<path stroke="white" stroke-width="0.5" fill="#000" d="M179.199,38.399c0,1.637-0.625,3.274-1.875,4.524l-85.076,85.075l85.076,85.075c2.5,2.5,2.5,6.55,0,9.05s-6.55,2.5-9.05,0 l-89.601-89.6c-2.5-2.5-2.5-6.551,0-9.051l89.601-89.6c2.5-2.5,6.55-2.5,9.05,0C178.574,35.124,179.199,36.762,179.199,38.399z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 652 B After Width: | Height: | Size: 649 B |
@@ -18,7 +18,8 @@ module.exports = {
|
||||
// clearMocks: false,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
collectCoverage: true,
|
||||
|
||||
collectCoverage: process.env.skipCodeCoverage === "true" ? false : true,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"],
|
||||
@@ -36,7 +37,7 @@ module.exports = {
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
functions: 24,
|
||||
lines: 28,
|
||||
statements: 28,
|
||||
},
|
||||
|
||||
@@ -3,337 +3,337 @@
|
||||
@import "../Common/Constants";
|
||||
|
||||
.query-panel {
|
||||
display: table;
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-top: 1px solid #DDDDDD;
|
||||
/*[{environment-commandbar-toolbar-separator}]*/
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
padding: 2px 0px 0px 2px;
|
||||
resize: vertical;
|
||||
display: table;
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-top: 1px solid #dddddd;
|
||||
/*[{environment-commandbar-toolbar-separator}]*/
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
padding: 2px 0px 0px 2px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.query-panel .row {
|
||||
display: table-row;
|
||||
display: table-row;
|
||||
}
|
||||
|
||||
.query-panel .row .cell {
|
||||
display: table-cell;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.query-panel.transition-in {
|
||||
display: table;
|
||||
top: 0px;
|
||||
-webkit-transition: top 2s linear;
|
||||
-ms-transition: top 2s linear;
|
||||
-moz-transition: top 2s linear;
|
||||
-khtml-transition: top 2s linear;
|
||||
-o-transition: top 2s linear;
|
||||
transition: top 2s linear;
|
||||
display: table;
|
||||
top: 0px;
|
||||
-webkit-transition: top 2s linear;
|
||||
-ms-transition: top 2s linear;
|
||||
-moz-transition: top 2s linear;
|
||||
-khtml-transition: top 2s linear;
|
||||
-o-transition: top 2s linear;
|
||||
transition: top 2s linear;
|
||||
}
|
||||
|
||||
.query-builder {
|
||||
width:100%;
|
||||
padding-right: @DefaultSpace;
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
margin-bottom: @DefaultSpace;
|
||||
width: 100%;
|
||||
padding-right: @DefaultSpace;
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
.query-builder-toolbar {
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
min-width: 600px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid #DDDDDD;
|
||||
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
min-width: 600px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
margin: 2px 0px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
margin: 2px 0px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button {
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
margin-left: 2px;
|
||||
background-color: transparent;
|
||||
border: solid transparent;
|
||||
min-width: 0px;
|
||||
padding: 0px;
|
||||
margin-left: 2px;
|
||||
background-color: transparent;
|
||||
border: solid transparent;
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:active {
|
||||
outline: 2px solid dodgerblue;
|
||||
/*[2px solid {common-common-controls-button-border-hover}]*/
|
||||
outline: 2px solid dodgerblue;
|
||||
/*[2px solid {common-common-controls-button-border-hover}]*/
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
||||
background-color: #CCCEDB;
|
||||
/*[{common-controls-button-hover-background}]*/
|
||||
background-color: #cccedb;
|
||||
/*[{common-controls-button-hover-background}]*/
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
||||
background-color: #E6E7ED;
|
||||
/*[{common-controls-inner-tab-active-background}]*/
|
||||
outline: none
|
||||
background-color: #e6e7ed;
|
||||
/*[{common-controls-inner-tab-active-background}]*/
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.disabled {
|
||||
background-color: #ffffff;
|
||||
/*[{common-controls-button-disabled-background}]*/
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: 0.4;
|
||||
background-color: #ffffff;
|
||||
/*[{common-controls-button-disabled-background}]*/
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
outline: none;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.tableContainer {
|
||||
overflow: hidden;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
overflow: hidden;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
}
|
||||
|
||||
.tablesQueryTab{
|
||||
padding-left: @MediumSpace;
|
||||
width: 100%;
|
||||
margin-bottom:@LargeSpace;
|
||||
.tablesQueryTab {
|
||||
padding-left: @MediumSpace;
|
||||
width: 100%;
|
||||
margin-bottom: @LargeSpace;
|
||||
}
|
||||
|
||||
.entity-error-Img {
|
||||
width: @WarningErrorIconSize;
|
||||
height: @WarningErrorIconSize;
|
||||
margin: @DefaultSpace 0px 0px @SmallSpace;
|
||||
width: @WarningErrorIconSize;
|
||||
height: @WarningErrorIconSize;
|
||||
margin: @DefaultSpace 0px 0px @SmallSpace;
|
||||
}
|
||||
|
||||
.query-editor-panel {
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
margin-top: 25px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
margin-top: 25px;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.query-editor-text {
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
border: solid 1px #A9ACB3;
|
||||
/*[{plugin-textbox-disabled-color}]*/
|
||||
resize: none;
|
||||
margin-top: -39px;
|
||||
background-color: #ddd;
|
||||
padding: 5px;
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
border: solid 1px #a9acb3;
|
||||
/*[{plugin-textbox-disabled-color}]*/
|
||||
resize: none;
|
||||
margin-top: -39px;
|
||||
background-color: #ddd;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.error-bar {
|
||||
padding: @LargeSpace 34px @MediumSpace 24px;
|
||||
padding: @LargeSpace 34px @MediumSpace 24px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: @BaseLow;
|
||||
padding: @DefaultSpace;
|
||||
display: inline-flex;
|
||||
background-color: @BaseLow;
|
||||
padding: @DefaultSpace;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
padding-left: @MediumSpace;
|
||||
padding-left: @MediumSpace;
|
||||
}
|
||||
|
||||
.query-editor-text-invalid {
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
border: 1px solid #e51400;
|
||||
resize: none;
|
||||
margin-top: -30px;
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
border: 1px solid #e51400;
|
||||
resize: none;
|
||||
margin-top: -30px;
|
||||
}
|
||||
|
||||
.query-editor-panel .warning-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
}
|
||||
|
||||
.query-editor-panel .warning-bar .warning-message {
|
||||
display: inline-flex;
|
||||
padding-top: 2px;
|
||||
vertical-align: middle;
|
||||
display: inline-flex;
|
||||
padding-top: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.query-editor-panel .warning-bar .warning-message .warning-text {
|
||||
margin-left: 2px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.advanced-options-panel{
|
||||
margin-bottom: @DefaultSpace;
|
||||
.advanced-options-panel {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-heading .advanced-title {
|
||||
display: inline-flex;
|
||||
margin-left: 27px;
|
||||
margin-top: 10px;
|
||||
cursor: default;
|
||||
display: inline-flex;
|
||||
margin-left: 27px;
|
||||
margin-top: 10px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options {
|
||||
margin-left: 32px;
|
||||
margin-top: 5px;
|
||||
border: 1px solid transparent;
|
||||
margin-left: 32px;
|
||||
margin-top: 5px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
border: 0;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options .top .top-input {
|
||||
width: 100px;
|
||||
word-spacing: normal;
|
||||
color: #1E1E1E;
|
||||
/*[{common-controls-button-foreground}]*/
|
||||
border: 1px solid #CCCEDB;
|
||||
/*[1px solid {plugin-textbox-border-color}]*/
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
width: 100px;
|
||||
word-spacing: normal;
|
||||
color: #1e1e1e;
|
||||
/*[{common-controls-button-foreground}]*/
|
||||
border: 1px solid #cccedb;
|
||||
/*[1px solid {plugin-textbox-border-color}]*/
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options .top .invalid-top {
|
||||
color: red;
|
||||
color: red;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options .select {
|
||||
margin-top: 18px;
|
||||
display: inline-flex;
|
||||
margin-top: 18px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.advanced-options-icon {
|
||||
margin-left: 2px;
|
||||
vertical-align: sub;
|
||||
margin-left: 2px;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options .select .select-options-text {
|
||||
margin-left: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.advanced-options-panel .advanced-options .select .select-options-link {
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.query-panel .row .column-headers .Field {
|
||||
padding-left: 95px;
|
||||
padding-right: 0px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 95px;
|
||||
padding-right: 0px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.clause-table {
|
||||
border-spacing: 0px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: -3px;
|
||||
border-spacing: 0px;
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.clause-table-row {
|
||||
display: row;
|
||||
margin-bottom: 10px;
|
||||
display: row;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.clause-table-cell {
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
display: table-cell;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.action-column>button,
|
||||
.group-control-header>button,
|
||||
.group-indicator-column>button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
cursor: pointer;
|
||||
.action-column > button,
|
||||
.group-control-header > button,
|
||||
.group-indicator-column > button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-control-header>button:disabled {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
outline: none;
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
.group-control-header > button:disabled {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
outline: none;
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.clause-table-field {
|
||||
width: 100%;
|
||||
border: 1px solid #bbbbbb;
|
||||
width: 100%;
|
||||
border: 1px solid #bbbbbb;
|
||||
}
|
||||
|
||||
.clause-table-cell button {
|
||||
height: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.clause-table-cell input[type="checkbox"] {
|
||||
padding: 0px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.and-or-svg {
|
||||
margin-top: -8px;
|
||||
margin-right: -5px;
|
||||
margin-top: -8px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
border-bottom: 1px transparent #DDD;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
border-top: 1px transparent #DDD;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
max-height: 20vh;
|
||||
width: 100%;
|
||||
border-bottom: 1px transparent #ddd;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
border-top: 1px transparent #ddd;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
max-height: 20vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.and-or-column,
|
||||
.and-or-header {
|
||||
min-width: 65px;
|
||||
padding-right: 10px;
|
||||
padding-left: 5px;
|
||||
min-width: 65px;
|
||||
padding-right: 10px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.operator-column,
|
||||
.operator-header {
|
||||
min-width: 65px;
|
||||
padding-right: 10px;
|
||||
min-width: 65px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.field-header,
|
||||
.field-column {
|
||||
min-width: 125px;
|
||||
padding-right: 10px;
|
||||
min-width: 125px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.type-header,
|
||||
.type-column {
|
||||
min-width: 85px;
|
||||
min-width: 85px;
|
||||
}
|
||||
|
||||
.and-or-column,
|
||||
@@ -345,41 +345,41 @@ input::-webkit-inner-spin-button {
|
||||
.type-header,
|
||||
.type-column,
|
||||
.action-header {
|
||||
padding-right: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding-right: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.value-header,
|
||||
.value-column,
|
||||
.time-column {
|
||||
min-width: 230px;
|
||||
padding: 0px 4px 0px 0px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
min-width: 230px;
|
||||
padding: 0px 4px 0px 0px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.group-control-header,
|
||||
.group-control-column {
|
||||
min-width: 25px;
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.group-indicator-table {
|
||||
border-spacing: 0px;
|
||||
min-height: 24px
|
||||
border-spacing: 0px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.group-indicator-column {
|
||||
min-width: 21px;
|
||||
padding: 0px;
|
||||
border-style: none;
|
||||
height: 29px;
|
||||
min-width: 21px;
|
||||
padding: 0px;
|
||||
border-style: none;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.clause-table-cell.action-column,
|
||||
.clause-table-cell.action-column,
|
||||
.clause-table-cell.action-header {
|
||||
min-width: 60px;
|
||||
padding-left: @SmallSpace;
|
||||
min-width: 60px;
|
||||
padding-left: @SmallSpace;
|
||||
}
|
||||
|
||||
.action-header,
|
||||
@@ -388,15 +388,14 @@ input::-webkit-inner-spin-button {
|
||||
.operator-header,
|
||||
.value-header,
|
||||
.and-or-header {
|
||||
padding-right: 4px;
|
||||
padding-bottom: 5px;
|
||||
padding-right: 4px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.header-background {
|
||||
background-color: #ffffff;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
/*.type-header {
|
||||
padding-right: 4px;
|
||||
}
|
||||
@@ -410,112 +409,111 @@ input::-webkit-inner-spin-button {
|
||||
}*/
|
||||
|
||||
.clause-table-field[readonly] {
|
||||
background-color: #EEEEF2;
|
||||
/*[{plugin-table-header-background-color}]*/
|
||||
border: 1px solid #CCCEDB;
|
||||
/*[{plugin-table-border-color}]*/
|
||||
background-color: #eeeef2;
|
||||
/*[{plugin-table-header-background-color}]*/
|
||||
border: 1px solid #cccedb;
|
||||
/*[{plugin-table-border-color}]*/
|
||||
}
|
||||
|
||||
.addClause-title {
|
||||
/*[{common-common-controls-button-border-hover}]*/
|
||||
cursor: pointer;
|
||||
margin-left: -5px;
|
||||
/*[{common-common-controls-button-border-hover}]*/
|
||||
cursor: pointer;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
.addClause {
|
||||
width: 125px;
|
||||
padding: 8px 0px 5px 5px;
|
||||
border: 1px solid #fff;
|
||||
margin-left: 5px;
|
||||
width: 125px;
|
||||
padding: 8px 0px 5px 5px;
|
||||
border: 1px solid #fff;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.addClause:hover {
|
||||
.hover();
|
||||
.hover();
|
||||
}
|
||||
|
||||
.addClause:active {
|
||||
.active();
|
||||
border: 1px dashed @AccentMedium;
|
||||
.active();
|
||||
border: 1px dashed @AccentMedium;
|
||||
}
|
||||
|
||||
.clause-table-row {
|
||||
min-width: 550px;
|
||||
width: 100%;
|
||||
min-width: 550px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clause-table-field field-column {
|
||||
min-width: 75px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
min-width: 75px;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clause-table-field field-input {
|
||||
min-width: 54px;
|
||||
margin-left: -78px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
min-width: 54px;
|
||||
margin-left: -78px;
|
||||
height: 25px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.query-panel .row .spacing {
|
||||
padding-bottom: 6px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.query-panel .divider.horizontal {
|
||||
height: 10px;
|
||||
width: 100%
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-div {
|
||||
display: inline
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.querybuilder-addpropertyImg,
|
||||
.querybuilder-cancelImg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 3px;
|
||||
margin-bottom: 8px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-left: 3px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.addclauseProperty-Img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 12px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.entity-Add-Cancel {
|
||||
padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||
cursor: pointer;
|
||||
// padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.entity-Add-Cancel:hover {
|
||||
.hover();
|
||||
.hover();
|
||||
}
|
||||
|
||||
.entity-Add-Cancel:active {
|
||||
.active();
|
||||
.active();
|
||||
}
|
||||
|
||||
.query-builder-isDisabled {
|
||||
border: 1px solid #CCCEDB;
|
||||
color: #ccc;
|
||||
border: 1px solid #cccedb;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.edit-value-text {
|
||||
padding-left: @DefaultSpace;
|
||||
padding-left: @DefaultSpace;
|
||||
}
|
||||
|
||||
.expand-triangle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.expand-triangle-right {
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.clause-table {
|
||||
@@ -524,4 +522,4 @@ input::-webkit-inner-spin-button {
|
||||
width: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
}*/
|
||||
}*/
|
||||
|
||||
@@ -2576,6 +2576,10 @@ a:link {
|
||||
.querydropdown.placeholderVisible {
|
||||
font-style: italic;
|
||||
}
|
||||
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: #767474;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.querydropdown:hover {
|
||||
background-color: @AccentLow;
|
||||
@@ -3087,3 +3091,4 @@ a:link {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
overflow: auto;
|
||||
|
||||
.databaseHeader {
|
||||
padding: 1px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.collectionHeader {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.loadMoreHeader {
|
||||
color: RGB(5, 99, 193);
|
||||
}
|
||||
}
|
||||
|
||||
.notebookResourceTree {
|
||||
@@ -23,6 +28,4 @@
|
||||
.clickDisabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -212,7 +212,7 @@
|
||||
"strict:find": "node ./strict-null-checks/find.js",
|
||||
"strict:add": "node ./strict-null-checks/auto-add.js",
|
||||
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
|
||||
"generateARMClients": "npx ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
|
||||
"generateARMClients": "npx ts-node utils/armClientGenerator/generator.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
11484
sampleData/queryCopilotSampleData.json
Normal file
11484
sampleData/queryCopilotSampleData.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
@@ -36,7 +36,7 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
id="collapseToggleLeftPaneButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Expand Tree"
|
||||
aria-label={getApiShortDisplayName() + `Expand tree`}
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
ref={focusButton}
|
||||
@@ -45,7 +45,7 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||
</span>
|
||||
<span className="collectionCollapsed">
|
||||
<span>{userContext.apiType} API</span>
|
||||
<span>{getApiShortDisplayName()}</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -45,6 +45,8 @@ export class ArmResourceTypes {
|
||||
|
||||
export class BackendDefaults {
|
||||
public static partitionKeyKind = "Hash";
|
||||
public static partitionKeyMultiHash = "MultiHash";
|
||||
public static maxNumMultiHashPartition = 2;
|
||||
public static singlePartitionStorageInGb: string = "10";
|
||||
public static multiPartitionStorageInGb: string = "100";
|
||||
public static maxChangeFeedRetentionDuration: number = 10;
|
||||
@@ -139,7 +141,7 @@ export class Queries {
|
||||
public static UnlimitedPageOption: string = "unlimited";
|
||||
public static itemsPerPage: number = 100;
|
||||
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
|
||||
|
||||
public static containersPerPage: number = 50;
|
||||
public static QueryEditorMinHeightRatio: number = 0.1;
|
||||
public static QueryEditorMaxHeightRatio: number = 0.4;
|
||||
public static readonly DefaultMaxDegreeOfParallelism = 6;
|
||||
@@ -426,3 +428,91 @@ export class JunoEndpoints {
|
||||
public static readonly Prod = "https://tools.cosmos.azure.com";
|
||||
public static readonly Stage = "https://tools-staging.cosmos.azure.com";
|
||||
}
|
||||
|
||||
export const QueryCopilotSampleDatabaseId = "CopilotSampleDb";
|
||||
export const QueryCopilotSampleContainerId = "SampleContainer";
|
||||
|
||||
export const QueryCopilotSampleContainerSchema = {
|
||||
product: {
|
||||
sampleData: {
|
||||
id: "de6fadec-0384-43c8-93ea-16c0170b5460",
|
||||
name: "Premium Phone Mini (Red)",
|
||||
price: 652.04,
|
||||
category: "Electronics",
|
||||
description:
|
||||
"This Premium Phone Mini (Red) is designed by the company under agreement with the FCC, so we'd give it a pass in either direction, but no one should be using this handset without a compatible handset. All in all, this phone is one of the most affordable Android handsets out there at $100. Check them out.\n\n9. HTC One M9 4",
|
||||
stock: 74,
|
||||
countryOfOrigin: "Mexico",
|
||||
firstAvailable: "2018-07-07 17:42:28",
|
||||
priceHistory: [592.81],
|
||||
customerRatings: [
|
||||
{ username: "dannyhowell", stars: 1, date: "2022-03-12 17:01:23", verifiedUser: true },
|
||||
{ username: "lindsay26", stars: 1, date: "2022-12-29 07:18:20", verifiedUser: false },
|
||||
{ username: "smithmiguel", stars: 3, date: "2022-09-08 11:43:27", verifiedUser: false },
|
||||
{ username: "julie07", stars: 3, date: "2021-03-14 23:54:10", verifiedUser: true },
|
||||
{ username: "kelly93", stars: 3, date: "2022-11-05 12:45:43", verifiedUser: false },
|
||||
{ username: "katherinereynolds", stars: 2, date: "2022-09-14 11:49:36", verifiedUser: false },
|
||||
{ username: "chandlerkenneth", stars: 1, date: "2022-01-14 12:14:43", verifiedUser: true },
|
||||
],
|
||||
rareProperty: true,
|
||||
},
|
||||
schema: {
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
},
|
||||
name: {
|
||||
type: "string",
|
||||
},
|
||||
price: {
|
||||
type: "number",
|
||||
},
|
||||
category: {
|
||||
type: "string",
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
stock: {
|
||||
type: "number",
|
||||
},
|
||||
countryOfOrigin: {
|
||||
type: "string",
|
||||
},
|
||||
firstAvailable: {
|
||||
type: "string",
|
||||
},
|
||||
priceHistory: {
|
||||
items: {
|
||||
type: "number",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
customerRatings: {
|
||||
items: {
|
||||
properties: {
|
||||
username: {
|
||||
type: "string",
|
||||
},
|
||||
stars: {
|
||||
type: "number",
|
||||
},
|
||||
date: {
|
||||
type: "string",
|
||||
},
|
||||
verifiedUser: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
type: "array",
|
||||
},
|
||||
rareProperty: {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
type: "object",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,7 +55,6 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
label={entityValueLabel && entityValueLabel}
|
||||
className="addEntityTextField"
|
||||
id="entityValueId"
|
||||
autoFocus
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
|
||||
14
src/Common/EnvironmentUtility.test.ts
Normal file
14
src/Common/EnvironmentUtility.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import * as EnvironmentUtility from "./EnvironmentUtility";
|
||||
|
||||
describe("Environment Utility Test", () => {
|
||||
it("Test sample URI with /", () => {
|
||||
const uri = "test/";
|
||||
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(uri);
|
||||
});
|
||||
|
||||
it("Test sample URI without /", () => {
|
||||
const uri = "test";
|
||||
const expectedResult = "test/";
|
||||
expect(EnvironmentUtility.normalizeArmEndpoint(uri)).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as OfferUtility from "./OfferUtility";
|
||||
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
|
||||
import { OfferResponse } from "@azure/cosmos";
|
||||
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
|
||||
import * as OfferUtility from "./OfferUtility";
|
||||
|
||||
describe("parseSDKOfferResponse", () => {
|
||||
it("manual throughput", () => {
|
||||
@@ -31,6 +31,26 @@ describe("parseSDKOfferResponse", () => {
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("offerContent not defined", () => {
|
||||
const mockOfferDefinition = {
|
||||
id: "test",
|
||||
} as SDKOfferDefinition;
|
||||
|
||||
const mockResponse = {
|
||||
resource: mockOfferDefinition,
|
||||
} as OfferResponse;
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("offerDefinition is null", () => {
|
||||
const mockResponse = {
|
||||
resource: undefined,
|
||||
} as OfferResponse;
|
||||
|
||||
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("autoscale throughput", () => {
|
||||
const mockOfferDefinition = {
|
||||
content: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getApiShortDisplayName } from "../Utils/APITypeUtils";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeContainerProps {
|
||||
@@ -42,7 +43,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
{/* Collections Window Title/Command Bar - Start */}
|
||||
<div className="collectiontitle">
|
||||
<div className="coltitle">
|
||||
<span className="titlepadcol">{userContext.apiType} API</span>
|
||||
<span className="titlepadcol">{getApiShortDisplayName()}</span>
|
||||
<div className="float-right">
|
||||
<span
|
||||
className="padimgcolrefresh"
|
||||
@@ -50,7 +51,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
role="button"
|
||||
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
|
||||
tabIndex={0}
|
||||
aria-label="Refresh tree"
|
||||
aria-label={getApiShortDisplayName() + `Refresh tree`}
|
||||
title="Refresh tree"
|
||||
>
|
||||
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
|
||||
@@ -62,7 +63,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
tabIndex={0}
|
||||
aria-label="Collapse Tree"
|
||||
aria-label={getApiShortDisplayName() + `Collapse Tree`}
|
||||
title="Collapse Tree"
|
||||
ref={focusButton}
|
||||
>
|
||||
|
||||
@@ -73,6 +73,17 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
|
||||
const sectionStackTokens: IStackTokens = { childrenGap: 12 };
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter" || event.key === "Space") {
|
||||
onEditEntity();
|
||||
}
|
||||
};
|
||||
const handleKeyPressdelete = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter" || event.key === "Space") {
|
||||
onDeleteEntity();
|
||||
}
|
||||
};
|
||||
|
||||
const getEntityValueType = (): string => {
|
||||
const { Int, Smallint, Tinyint } = CassandraType;
|
||||
const { Double, Int32, Int64 } = TableType;
|
||||
@@ -126,12 +137,29 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
|
||||
/>
|
||||
{!isEntityValueDisable && (
|
||||
<TooltipHost content="Edit property" id="editTooltip">
|
||||
<Image {...imageProps} src={EditIcon} alt="editEntity" id="editEntity" onClick={onEditEntity} />
|
||||
<div tabIndex={0}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EditIcon}
|
||||
alt="editEntity"
|
||||
onClick={onEditEntity}
|
||||
tabIndex={0}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
</div>
|
||||
</TooltipHost>
|
||||
)}
|
||||
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (
|
||||
<TooltipHost content="Delete property" id="deleteTooltip">
|
||||
<Image {...imageProps} src={DeleteIcon} alt="delete entity" id="deleteEntity" onClick={onDeleteEntity} />
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={DeleteIcon}
|
||||
alt="delete entity"
|
||||
id="deleteEntity"
|
||||
onClick={onDeleteEntity}
|
||||
tabIndex={0}
|
||||
onKeyPress={handleKeyPressdelete}
|
||||
/>
|
||||
</TooltipHost>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
||||
return (
|
||||
<span>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon iconName="Info" ariaLabel={children} className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
49
src/Common/UrlUtility.test.ts
Normal file
49
src/Common/UrlUtility.test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as UrlUtility from "./UrlUtility";
|
||||
|
||||
describe("parseDocumentsPath", () => {
|
||||
it("empty resource path", () => {
|
||||
const resourcePath = "";
|
||||
|
||||
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual({});
|
||||
});
|
||||
|
||||
it("resourcePath does not begin or end with /", () => {
|
||||
const resourcePath = "localhost/portal/home";
|
||||
const expectedResult = {
|
||||
type: "home",
|
||||
objectBody: {
|
||||
id: "portal",
|
||||
self: "/localhost/portal/home/",
|
||||
},
|
||||
};
|
||||
|
||||
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("resourcePath length is even", () => {
|
||||
const resourcePath = "/localhost/portal/src/home/";
|
||||
const expectedResult = {
|
||||
type: "src",
|
||||
objectBody: {
|
||||
id: "home",
|
||||
self: resourcePath,
|
||||
},
|
||||
};
|
||||
|
||||
expect(UrlUtility.parseDocumentsPath(resourcePath)).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("createUri", () => {
|
||||
const baseUri = "http://foo.com/bar/";
|
||||
const relativeUri = "/index.html";
|
||||
const expectedUri = "http://foo.com/bar/index.html";
|
||||
|
||||
expect(UrlUtility.createUri(baseUri, relativeUri)).toEqual(expectedUri);
|
||||
});
|
||||
|
||||
it("should throw an error if baseUri is empty", () => {
|
||||
expect(() => {
|
||||
UrlUtility.createUri("", "/home");
|
||||
}).toThrow("baseUri is null or empty");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
|
||||
export const deleteDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<void> => {
|
||||
const entityName: string = getEntityName();
|
||||
@@ -13,7 +14,7 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc
|
||||
await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
.delete();
|
||||
logConsoleInfo(`Successfully deleted ${entityName} ${documentId.id()}`);
|
||||
} catch (error) {
|
||||
|
||||
12
src/Common/dataAccess/getPartitionKeyValue.ts
Normal file
12
src/Common/dataAccess/getPartitionKeyValue.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { userContext } from "UserContext";
|
||||
import DocumentId from "../../Explorer/Tree/DocumentId";
|
||||
|
||||
export const getPartitionKeyValue = (documentId: DocumentId) => {
|
||||
if (userContext.apiType === "Tables" && documentId.partitionKeyValue?.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (documentId.partitionKeyValue?.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return documentId.partitionKeyValue;
|
||||
};
|
||||
@@ -96,6 +96,14 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
const instantMaximumThroughput: number =
|
||||
typeof resource.instantMaximumThroughput === "string"
|
||||
? parseInt(resource.instantMaximumThroughput)
|
||||
: resource.instantMaximumThroughput;
|
||||
const softAllowedMaximumThroughput: number =
|
||||
typeof resource.softAllowedMaximumThroughput === "string"
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
@@ -104,6 +112,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +123,8 @@ const readCollectionOfferWithARM = async (databaseId: string, collectionId: stri
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Queries } from "Common/Constants";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
@@ -31,6 +32,35 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
|
||||
}
|
||||
}
|
||||
|
||||
export async function readCollectionsWithPagination(
|
||||
databaseId: string,
|
||||
continuationToken?: string
|
||||
): Promise<DataModels.CollectionsWithPagination> {
|
||||
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||
try {
|
||||
const sdkResponse = await client()
|
||||
.database(databaseId)
|
||||
.containers.query(
|
||||
{ query: "SELECT * FROM c" },
|
||||
{
|
||||
continuationToken,
|
||||
maxItemCount: Queries.containersPerPage,
|
||||
}
|
||||
)
|
||||
.fetchNext();
|
||||
const collectionsWithPagination: DataModels.CollectionsWithPagination = {
|
||||
collections: sdkResponse.resources as DataModels.Collection[],
|
||||
continuationToken: sdkResponse.continuationToken,
|
||||
};
|
||||
return collectionsWithPagination;
|
||||
} catch (error) {
|
||||
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
|
||||
throw error;
|
||||
} finally {
|
||||
clearMessage();
|
||||
}
|
||||
}
|
||||
|
||||
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
|
||||
let rpResponse;
|
||||
|
||||
|
||||
@@ -68,6 +68,14 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
? parseInt(resource.minimumThroughput)
|
||||
: resource.minimumThroughput;
|
||||
const autoscaleSettings = resource.autoscaleSettings;
|
||||
const instantMaximumThroughput: number =
|
||||
typeof resource.instantMaximumThroughput === "string"
|
||||
? parseInt(resource.instantMaximumThroughput)
|
||||
: resource.instantMaximumThroughput;
|
||||
const softAllowedMaximumThroughput: number =
|
||||
typeof resource.softAllowedMaximumThroughput === "string"
|
||||
? parseInt(resource.softAllowedMaximumThroughput)
|
||||
: resource.softAllowedMaximumThroughput;
|
||||
|
||||
if (autoscaleSettings) {
|
||||
return {
|
||||
@@ -76,6 +84,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -85,6 +95,8 @@ const readDatabaseOfferWithARM = async (databaseId: string): Promise<Offer> => {
|
||||
manualThroughput: resource.throughput,
|
||||
minimumThroughput,
|
||||
offerReplacePending: resource.offerReplacePending === "true",
|
||||
instantMaximumThroughput,
|
||||
softAllowedMaximumThroughput,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { HttpHeaders } from "../Constants";
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
|
||||
export const readDocument = async (collection: CollectionBase, documentId: DocumentId): Promise<Item> => {
|
||||
const entityName = getEntityName();
|
||||
@@ -21,8 +22,7 @@ export const readDocument = async (collection: CollectionBase, documentId: Docum
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
// use undefined if the partitionKeyValue is empty
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
.read(options);
|
||||
|
||||
return response?.resource;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import { CloudError, SqlStoredProcedureListResult } from "Utils/arm/generatedClients/cosmos/types";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { listSqlStoredProcedures } from "../../Utils/arm/generatedClients/cosmos/sqlResources";
|
||||
import { client } from "../CosmosClient";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
|
||||
@@ -24,7 +25,13 @@ export async function readStoredProcedures(
|
||||
databaseId,
|
||||
collectionId
|
||||
);
|
||||
return rpResponse?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
const listResult = rpResponse as SqlStoredProcedureListResult;
|
||||
if (listResult) {
|
||||
return listResult?.value?.map((sproc) => sproc.properties?.resource as StoredProcedureDefinition & Resource);
|
||||
}
|
||||
|
||||
const cloudError = rpResponse as CloudError;
|
||||
throw new Error(cloudError?.error?.message);
|
||||
}
|
||||
|
||||
const response = await client()
|
||||
|
||||
@@ -6,6 +6,7 @@ import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationCons
|
||||
import { client } from "../CosmosClient";
|
||||
import { getEntityName } from "../DocumentUtility";
|
||||
import { handleError } from "../ErrorHandlingUtils";
|
||||
import { getPartitionKeyValue } from "./getPartitionKeyValue";
|
||||
|
||||
export const updateDocument = async (
|
||||
collection: CollectionBase,
|
||||
@@ -25,7 +26,7 @@ export const updateDocument = async (
|
||||
const response = await client()
|
||||
.database(collection.databaseId)
|
||||
.container(collection.id())
|
||||
.item(documentId.id(), documentId.partitionKeyValue?.length === 0 ? undefined : documentId.partitionKeyValue)
|
||||
.item(documentId.id(), getPartitionKeyValue(documentId))
|
||||
.replace(newDocument, options);
|
||||
|
||||
logConsoleInfo(`Successfully updated ${entityName} ${documentId.id()}`);
|
||||
|
||||
@@ -33,6 +33,8 @@ export interface DatabaseAccountExtendedProperties {
|
||||
privateEndpointConnections?: unknown[];
|
||||
capacity?: { totalThroughputLimit: number };
|
||||
locations?: DatabaseAccountResponseLocation[];
|
||||
postgresqlEndpoint?: string;
|
||||
publicNetworkAccess?: string;
|
||||
}
|
||||
|
||||
export interface DatabaseAccountResponseLocation {
|
||||
@@ -154,6 +156,11 @@ export interface Collection extends Resource {
|
||||
requestSchema?: () => void;
|
||||
}
|
||||
|
||||
export interface CollectionsWithPagination {
|
||||
continuationToken?: string;
|
||||
collections?: Collection[];
|
||||
}
|
||||
|
||||
export interface Database extends Resource {
|
||||
collections?: Collection[];
|
||||
}
|
||||
@@ -235,6 +242,8 @@ export interface Offer {
|
||||
minimumThroughput: number | undefined;
|
||||
offerDefinition?: SDKOfferDefinition;
|
||||
offerReplacePending: boolean;
|
||||
instantMaximumThroughput?: number;
|
||||
softAllowedMaximumThroughput?: number;
|
||||
}
|
||||
|
||||
export interface SDKOfferDefinition extends Resource {
|
||||
@@ -566,6 +575,16 @@ export interface ContainerConnectionInfo {
|
||||
//need to add ram and rom info
|
||||
}
|
||||
|
||||
export interface PostgresFirewallRule {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: {
|
||||
startIpAddress: string;
|
||||
endIpAddress: string;
|
||||
};
|
||||
}
|
||||
|
||||
export enum PhoenixErrorType {
|
||||
MaxAllocationTimeExceeded = "MaxAllocationTimeExceeded",
|
||||
MaxDbAccountsPerUserExceeded = "MaxDbAccountsPerUserExceeded",
|
||||
|
||||
@@ -35,6 +35,9 @@ export enum MessageTypes {
|
||||
RefreshDatabaseAccount,
|
||||
CloseTab,
|
||||
OpenQuickstartBlade,
|
||||
OpenPostgreSQLPasswordReset,
|
||||
OpenPostgresNetworkingBlade,
|
||||
OpenCosmosDBNetworkingBlade,
|
||||
}
|
||||
|
||||
export { Versions, ActionContracts, Diagnostics };
|
||||
|
||||
@@ -87,13 +87,13 @@ export interface Database extends TreeNode {
|
||||
isDatabaseExpanded: ko.Observable<boolean>;
|
||||
isDatabaseShared: ko.Computed<boolean>;
|
||||
isSampleDB?: boolean;
|
||||
|
||||
collectionsContinuationToken?: string;
|
||||
selectedSubnodeKind: ko.Observable<CollectionTabKind>;
|
||||
|
||||
expandDatabase(): Promise<void>;
|
||||
collapseDatabase(): void;
|
||||
|
||||
loadCollections(): Promise<void>;
|
||||
loadCollections(restart?: boolean): Promise<void>;
|
||||
findCollectionWithId(collectionId: string): Collection;
|
||||
openAddCollection(database: Database, event: MouseEvent): void;
|
||||
onSettingsClick: () => void;
|
||||
@@ -186,7 +186,6 @@ export interface Collection extends CollectionBase {
|
||||
onDrop(source: Collection, event: { originalEvent: DragEvent }): void;
|
||||
uploadFiles(fileList: FileList): Promise<{ data: UploadDetailsRecord[] }>;
|
||||
|
||||
getLabel(): string;
|
||||
getPendingThroughputSplitNotification(): Promise<DataModels.Notification>;
|
||||
}
|
||||
|
||||
@@ -372,6 +371,7 @@ export enum TerminalKind {
|
||||
Default = 0,
|
||||
Mongo = 1,
|
||||
Cassandra = 2,
|
||||
Postgres = 3,
|
||||
}
|
||||
|
||||
export interface DataExplorerInputsFrame {
|
||||
@@ -395,6 +395,11 @@ export interface DataExplorerInputsFrame {
|
||||
sharedThroughputDefault?: number;
|
||||
dataExplorerVersion?: string;
|
||||
defaultCollectionThroughput?: CollectionCreationDefaults;
|
||||
isPostgresAccount?: boolean;
|
||||
isReplica?: boolean;
|
||||
clientIpAddress?: string;
|
||||
// TODO: Update this param in the OSS extension to remove isFreeTier, isMarlinServerGroup, and make nodes a flat array instead of an nested array
|
||||
connectionStringParams?: any;
|
||||
flights?: readonly string[];
|
||||
features?: {
|
||||
[key: string]: string;
|
||||
|
||||
@@ -11,9 +11,9 @@ import DeleteTriggerIcon from "../../images/DeleteTrigger.svg";
|
||||
import DeleteUDFIcon from "../../images/DeleteUDF.svg";
|
||||
import HostedTerminalIcon from "../../images/Hosted-Terminal.svg";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName } from "../Utils/APITypeUtils";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { TreeNodeMenuItem } from "./Controls/TreeComponent/TreeComponent";
|
||||
import Explorer from "./Explorer";
|
||||
import { useNotebook } from "./Notebook/useNotebook";
|
||||
@@ -152,7 +152,7 @@ export const createStoreProcedureContextMenuItems = (
|
||||
{
|
||||
iconSrc: DeleteSprocIcon,
|
||||
onClick: () => storedProcedure.delete(),
|
||||
label: "Delete Store Procedure",
|
||||
label: "Delete Stored Procedure",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
<img
|
||||
className="expandCollapseIcon"
|
||||
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
|
||||
alt="Hide"
|
||||
alt={this.state.isExpanded ? `${this.props.title} hide` : `${this.props.title} expand`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Wrapper around Notebook server terminal
|
||||
*/
|
||||
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import postRobot from "post-robot";
|
||||
import * as React from "react";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
@@ -40,6 +41,7 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
|
||||
handleFrameLoad(event: React.SyntheticEvent<HTMLIFrameElement, Event>): void {
|
||||
this.terminalWindow = (event.target as HTMLIFrameElement).contentWindow;
|
||||
useTerminal.getState().setTerminal(this.terminalWindow);
|
||||
this.sendPropsToTerminalFrame();
|
||||
}
|
||||
|
||||
@@ -74,6 +76,8 @@ export class NotebookTerminalComponent extends React.Component<NotebookTerminalC
|
||||
this.props.databaseAccount?.properties.mongoEndpoint || this.props.databaseAccount?.properties.documentEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "cassandra")) {
|
||||
terminalEndpoint = this.props.databaseAccount?.properties.cassandraEndpoint;
|
||||
} else if (StringUtils.endsWith(notebookServerEndpoint, "postgresql")) {
|
||||
return this.props.databaseAccount?.properties.postgresqlEndpoint;
|
||||
}
|
||||
|
||||
if (terminalEndpoint) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HoverCard, HoverCardType, Icon, Label, Link, Stack } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "@fluentui/react";
|
||||
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||
import "./InfoComponent.less";
|
||||
|
||||
@@ -41,7 +41,7 @@ export class InfoComponent extends React.Component<InfoComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||
<div className="infoPanelMain">
|
||||
<div className="infoPanelMain" tabIndex={0}>
|
||||
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||
<Label className="infoLabelMain">Help</Label>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ exports[`InfoComponent renders 1`] = `
|
||||
>
|
||||
<div
|
||||
className="infoPanelMain"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Icon
|
||||
className="infoIconMain"
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { IColumn, Text } from "@fluentui/react";
|
||||
import {
|
||||
getAutoPilotV3SpendElement,
|
||||
getEstimatedSpendingElement,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
ttlWarning,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
updateThroughputDelayedApplyWarningMessage,
|
||||
getThroughputApplyDelayedMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getToolTipContainer,
|
||||
conflictResolutionCustomToolTip,
|
||||
changeFeedPolicyToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
changeFeedPolicyToolTip,
|
||||
conflictResolutionCustomToolTip,
|
||||
conflictResolutionLwwTooltip,
|
||||
getEstimatedSpendingElement,
|
||||
getRuPriceBreakdown,
|
||||
getThroughputApplyDelayedMessage,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
getToolTipContainer,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
mongoIndexingPolicyAADError,
|
||||
mongoIndexingPolicyDisclaimer,
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ttlWarning,
|
||||
updateThroughputDelayedApplyWarningMessage,
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
public render(): JSX.Element {
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: <Text>$ 1.02</Text>,
|
||||
daily: <Text>$ 24.48</Text>,
|
||||
monthly: <Text>$ 744.6</Text>,
|
||||
},
|
||||
];
|
||||
const costElement: JSX.Element = <></>;
|
||||
const priceBreakdown: PriceBreakdown = {
|
||||
hourlyPrice: 1.02,
|
||||
dailyPrice: 24.48,
|
||||
@@ -52,17 +35,11 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
|
||||
return (
|
||||
<>
|
||||
{getAutoPilotV3SpendElement(1000, false)}
|
||||
{getAutoPilotV3SpendElement(undefined, false)}
|
||||
{getAutoPilotV3SpendElement(1000, true)}
|
||||
{getAutoPilotV3SpendElement(undefined, true)}
|
||||
|
||||
{getEstimatedSpendingElement(estimatedSpendingColumns, estimatedSpendingItems, 1000, 2, priceBreakdown, false)}
|
||||
{getEstimatedSpendingElement(costElement, 1000, 2, priceBreakdown, false)}
|
||||
|
||||
{manualToAutoscaleDisclaimerElement}
|
||||
{ttlWarning}
|
||||
{indexingPolicynUnsavedWarningMessage}
|
||||
{updateThroughputBeyondLimitWarningMessage}
|
||||
{updateThroughputDelayedApplyWarningMessage}
|
||||
|
||||
{getThroughputApplyDelayedMessage(false, 1000, "RU/s", "sampleDb", "sampleCollection", 2000)}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IChoiceGroupStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
@@ -20,7 +17,6 @@ import {
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
SelectionMode,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Stack,
|
||||
@@ -28,8 +24,7 @@ import {
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { hoursInAMonth } from "../../../Shared/Constants";
|
||||
import {
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
@@ -103,6 +98,10 @@ export const checkBoxAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 },
|
||||
};
|
||||
|
||||
export const relaxedSpacingStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 20 },
|
||||
};
|
||||
|
||||
export const toolTipLabelStackTokens: IStackTokens = {
|
||||
childrenGap: 6,
|
||||
};
|
||||
@@ -174,41 +173,6 @@ export function onRenderRow(props: IDetailsRowProps): JSX.Element {
|
||||
return <DetailsRow {...props} styles={transparentDetailsRowStyles} />;
|
||||
}
|
||||
|
||||
export const getAutoPilotV3SpendElement = (
|
||||
maxAutoPilotThroughputSet: number,
|
||||
isDatabaseThroughput: boolean,
|
||||
requestUnitsUsageCostElement?: JSX.Element
|
||||
): JSX.Element => {
|
||||
if (!maxAutoPilotThroughputSet) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const resource: string = isDatabaseThroughput ? "database" : "container";
|
||||
return (
|
||||
<>
|
||||
<Text>
|
||||
Your {resource} throughput will automatically scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(maxAutoPilotThroughputSet)} RU/s (10% of max RU/s) -{" "}
|
||||
{maxAutoPilotThroughputSet} RU/s
|
||||
</b>{" "}
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
{requestUnitsUsageCostElement}
|
||||
<Text>
|
||||
After the first {AutoPilotUtils.getStorageBasedOnUserInput(maxAutoPilotThroughputSet)} GB of data stored, the
|
||||
max RU/s will be automatically upgraded based on the new storage value.
|
||||
<Link href={AutopilotDocumentation.Url} target="_blank">
|
||||
{" "}
|
||||
Learn more
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getRuPriceBreakdown = (
|
||||
throughput: number,
|
||||
serverId: string,
|
||||
@@ -238,8 +202,7 @@ export const getRuPriceBreakdown = (
|
||||
};
|
||||
|
||||
export const getEstimatedSpendingElement = (
|
||||
estimatedSpendingColumns: IColumn[],
|
||||
estimatedSpendingItems: EstimatedSpendingDisplayProps[],
|
||||
costElement: JSX.Element,
|
||||
throughput: number,
|
||||
numberOfRegions: number,
|
||||
priceBreakdown: PriceBreakdown,
|
||||
@@ -247,22 +210,25 @@ export const getEstimatedSpendingElement = (
|
||||
): JSX.Element => {
|
||||
const ruRange: string = isAutoscale ? throughput / 10 + " RU/s - " : "";
|
||||
return (
|
||||
<Stack {...addMongoIndexStackProps} styles={mediumWidthStackStyles}>
|
||||
<DetailsList
|
||||
disableSelectionZone
|
||||
items={estimatedSpendingItems}
|
||||
columns={estimatedSpendingColumns}
|
||||
selectionMode={SelectionMode.none}
|
||||
layoutMode={DetailsListLayoutMode.justified}
|
||||
onRenderRow={onRenderRow}
|
||||
/>
|
||||
<Text id="throughputSpendElement">
|
||||
({"regions: "} {numberOfRegions}, {ruRange}
|
||||
{throughput} RU/s, {priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU)
|
||||
</Text>
|
||||
<Text>
|
||||
<em>{estimatedCostDisclaimer}</em>
|
||||
<Stack>
|
||||
<Text style={{ fontWeight: 600 }}>Cost estimate*</Text>
|
||||
{costElement}
|
||||
<Text style={{ fontWeight: 600, marginTop: 15 }}>How we calculate this</Text>
|
||||
<Stack id="throughputSpendElement" style={{ marginTop: 5 }}>
|
||||
<span>
|
||||
{numberOfRegions} region{numberOfRegions > 1 && <span>s</span>}
|
||||
</span>
|
||||
<span>
|
||||
{ruRange}
|
||||
{throughput} RU/s
|
||||
</span>
|
||||
<span>
|
||||
{priceBreakdown.currencySign}
|
||||
{priceBreakdown.pricePerRu}/RU
|
||||
</span>
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 15 }}>
|
||||
<em>*{estimatedCostDisclaimer}</em>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -293,14 +259,6 @@ export const indexingPolicynUnsavedWarningMessage: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputBeyondLimitWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputBeyondLimitWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and
|
||||
increase throughput for the selected container. This operation will take 1-3 business days to complete. You can
|
||||
track the status of this request in Notifications.
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. This operation will take some
|
||||
@@ -308,6 +266,61 @@ export const updateThroughputDelayedApplyWarningMessage: JSX.Element = (
|
||||
</Text>
|
||||
);
|
||||
|
||||
export const getUpdateThroughputBeyondInstantLimitMessage = (instantMaximumThroughput: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
Scaling up will take 4-6 hours as it exceeds what Azure Cosmos DB can instantly support currently based on your
|
||||
number of physical partitions. You can increase your throughput to {instantMaximumThroughput} instantly or proceed
|
||||
with this value and wait until the scale-up is completed.
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateThroughputBeyondSupportLimitMessage = (
|
||||
instantMaximumThroughput: number,
|
||||
maximumThroughput: number
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
<Text styles={infoAndToolTipTextStyle} id="updateThroughputDelayedApplyWarningMessage">
|
||||
Your request to increase throughput exceeds the pre-allocated capacity which may take longer than expected.
|
||||
There are three options you can choose from to proceed:
|
||||
</Text>
|
||||
<ol style={{ fontSize: 14, color: "windowtext", marginTop: "5px" }}>
|
||||
<li>You can instantly scale up to {instantMaximumThroughput} RU/s.</li>
|
||||
{instantMaximumThroughput < maximumThroughput && (
|
||||
<li>You can asynchronously scale up to any value under {maximumThroughput} RU/s in 4-6 hours.</li>
|
||||
)}
|
||||
<li>
|
||||
Your current quota max is {maximumThroughput} RU/s. To go over this limit, you must request a quota increase
|
||||
and the Azure Cosmos DB team will review.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/create-support-request-quota-increase"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getUpdateThroughputBelowMinimumMessage = (minimum: number): JSX.Element => {
|
||||
return (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
You are not able to lower throughput below your current minimum of {minimum} RU/s. For more information on this
|
||||
limit, please refer to our service quote documentation.
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/cosmos-db/concepts-limits#minimum-throughput-limits"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const saveThroughputWarningMessage: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle}>
|
||||
Your bill will be affected as you update your throughput settings. Please review the updated cost estimate below
|
||||
@@ -499,7 +512,11 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
|
||||
},
|
||||
});
|
||||
|
||||
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
|
||||
export const getChoiceGroupStyles = (
|
||||
current: isDirtyTypes,
|
||||
baseline: isDirtyTypes,
|
||||
isHorizontal?: boolean
|
||||
): Partial<IChoiceGroupStyles> => ({
|
||||
flexContainer: [
|
||||
{
|
||||
selectors: {
|
||||
@@ -516,6 +533,8 @@ export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTyp
|
||||
padding: "2px 5px",
|
||||
},
|
||||
},
|
||||
display: isHorizontal ? "inline-flex" : "default",
|
||||
columnGap: isHorizontal ? "30px" : "default",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import ko from "knockout";
|
||||
import React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
import { updateUserContext } from "../../../../UserContext";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { throughputUnit } from "../SettingsRenderUtils";
|
||||
@@ -12,7 +11,6 @@ import { ScaleComponent, ScaleComponentProps } from "./ScaleComponent";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
|
||||
describe("ScaleComponent", () => {
|
||||
const nonNationalCloudContainer = new Explorer();
|
||||
const targetThroughput = 6000;
|
||||
|
||||
const baseProps: ScaleComponentProps = {
|
||||
@@ -125,11 +123,4 @@ describe("ScaleComponent", () => {
|
||||
scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.canThroughputExceedMaximumValue()).toEqual(true);
|
||||
});
|
||||
|
||||
it("getThroughputWarningMessage", () => {
|
||||
const throughputBeyondLimit = SharedConstants.CollectionCreation.DefaultCollectionRUs1Million + 1000;
|
||||
const newProps = { ...baseProps, container: nonNationalCloudContainer, throughput: throughputBeyondLimit };
|
||||
const scaleComponent = new ScaleComponent(newProps);
|
||||
expect(scaleComponent.getThroughputWarningMessage().props.id).toEqual("updateThroughputBeyondLimitWarningMessage");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Label, Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
|
||||
import { Link, MessageBar, MessageBarType, Stack, Text, TextField } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../../Common/Constants";
|
||||
import { configContext, Platform } from "../../../../ConfigContext";
|
||||
import { Platform, configContext } from "../../../../ConfigContext";
|
||||
import * as DataModels from "../../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../../Contracts/ViewModels";
|
||||
import * as SharedConstants from "../../../../Shared/Constants";
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
subComponentStackProps,
|
||||
throughputUnit,
|
||||
titleAndInputStackProps,
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import { ThroughputInputAutoPilotV3Component } from "./ThroughputInputComponents/ThroughputInputAutoPilotV3Component";
|
||||
@@ -68,16 +67,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return !!enableAutoScaleCapability;
|
||||
};
|
||||
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixedContainer ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
<Text>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
public getMaxRUs = (): number => {
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
return Constants.TryCosmosExperience.maxRU;
|
||||
@@ -131,18 +120,6 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public getThroughputWarningMessage = (): JSX.Element => {
|
||||
const throughputExceedsBackendLimits: boolean =
|
||||
this.canThroughputExceedMaximumValue() &&
|
||||
this.props.throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million;
|
||||
|
||||
if (throughputExceedsBackendLimits && !this.props.isFixedContainer) {
|
||||
return updateThroughputBeyondLimitWarningMessage;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public getLongDelayMessage = (): JSX.Element => {
|
||||
const matches: string[] = this.props.initialNotification?.description.match(
|
||||
`Throughput update for (.*) ${throughputUnit}`
|
||||
@@ -188,9 +165,10 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
spendAckChecked={false}
|
||||
onScaleSaveableChange={this.props.onScaleSaveableChange}
|
||||
onScaleDiscardableChange={this.props.onScaleDiscardableChange}
|
||||
getThroughputWarningMessage={this.getThroughputWarningMessage}
|
||||
usageSizeInKB={this.props.collection?.usageSizeInKB()}
|
||||
throughputError={this.props.throughputError}
|
||||
instantMaximumThroughput={this.offer?.instantMaximumThroughput}
|
||||
softAllowedMaximumThroughput={this.offer?.softAllowedMaximumThroughput}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -229,12 +207,7 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
{this.getInitialNotificationElement() && (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>{this.getInitialNotificationElement()}</MessageBar>
|
||||
)}
|
||||
{!this.isAutoScaleEnabled() && (
|
||||
<Stack {...subComponentStackProps}>
|
||||
{this.getThroughputInputComponent()}
|
||||
{!this.props.database && this.getStorageCapacityTitle()}
|
||||
</Stack>
|
||||
)}
|
||||
{!this.isAutoScaleEnabled() && <Stack {...subComponentStackProps}>{this.getThroughputInputComponent()}</Stack>}
|
||||
|
||||
{/* TODO: Replace link with call to the Azure Support blade */}
|
||||
{this.isAutoScaleEnabled() && (
|
||||
|
||||
@@ -310,7 +310,16 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.isLargePartitionKeyEnabled() && <Text>Large {this.partitionKeyName.toLowerCase()} has been enabled</Text>}
|
||||
{userContext.apiType === "SQL" && this.isLargePartitionKeyEnabled() && (
|
||||
<Text>Large {this.partitionKeyName.toLowerCase()} has been enabled.</Text>
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" &&
|
||||
(this.isHierarchicalPartitionedContainer() ? (
|
||||
<Text>Hierarchically partitioned container.</Text>
|
||||
) : (
|
||||
<Text>Non-hierarchically partitioned container.</Text>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -328,6 +337,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
};
|
||||
|
||||
public isLargePartitionKeyEnabled = (): boolean => this.props.collection.partitionKey?.version >= 2;
|
||||
public isHierarchicalPartitionedContainer = (): boolean => this.props.collection.partitionKey?.kind === "MultiHash";
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,8 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
onScaleDiscardableChange: () => {
|
||||
return;
|
||||
},
|
||||
getThroughputWarningMessage: () => undefined,
|
||||
instantMaximumThroughput: 5000,
|
||||
softAllowedMaximumThroughput: 1000000,
|
||||
};
|
||||
|
||||
it("throughput input visible", () => {
|
||||
|
||||
@@ -3,10 +3,14 @@ import {
|
||||
ChoiceGroup,
|
||||
FontIcon,
|
||||
IChoiceGroupOption,
|
||||
IColumn,
|
||||
IProgressIndicatorStyles,
|
||||
ISeparatorStyles,
|
||||
Label,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
ProgressIndicator,
|
||||
Separator,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
@@ -23,24 +27,24 @@ import { autoPilotThroughput1K } from "../../../../../Utils/AutoPilotUtils";
|
||||
import { calculateEstimateNumber, usageInGB } from "../../../../../Utils/PricingUtils";
|
||||
import { Int32 } from "../../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
import {
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
checkBoxAndInputStackProps,
|
||||
getAutoPilotV3SpendElement,
|
||||
getChoiceGroupStyles,
|
||||
getEstimatedSpendingElement,
|
||||
getRuPriceBreakdown,
|
||||
getTextFieldStyles,
|
||||
getToolTipContainer,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
getUpdateThroughputBelowMinimumMessage,
|
||||
getUpdateThroughputBeyondInstantLimitMessage,
|
||||
getUpdateThroughputBeyondSupportLimitMessage,
|
||||
manualToAutoscaleDisclaimerElement,
|
||||
messageBarStyles,
|
||||
noLeftPaddingCheckBoxStyle,
|
||||
PriceBreakdown,
|
||||
relaxedSpacingStackProps,
|
||||
saveThroughputWarningMessage,
|
||||
titleAndInputStackProps,
|
||||
transparentDetailsHeaderStyle,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
import { IsComponentDirtyResult, getSanitizedInputValue, isDirty } from "../../SettingsUtils";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
|
||||
export interface ThroughputInputAutoPilotV3Props {
|
||||
@@ -73,16 +77,16 @@ export interface ThroughputInputAutoPilotV3Props {
|
||||
onMaxAutoPilotThroughputChange: (newThroughput: number) => void;
|
||||
onScaleSaveableChange: (isScaleSaveable: boolean) => void;
|
||||
onScaleDiscardableChange: (isScaleDiscardable: boolean) => void;
|
||||
getThroughputWarningMessage: () => JSX.Element;
|
||||
usageSizeInKB: number;
|
||||
throughputError?: string;
|
||||
instantMaximumThroughput: number;
|
||||
softAllowedMaximumThroughput: number;
|
||||
}
|
||||
|
||||
interface ThroughputInputAutoPilotV3State {
|
||||
spendAckChecked: boolean;
|
||||
exceedFreeTierThroughput: boolean;
|
||||
}
|
||||
|
||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
ThroughputInputAutoPilotV3Props,
|
||||
ThroughputInputAutoPilotV3State
|
||||
@@ -128,7 +132,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
} else if (this.props.isAutoPilotSelected) {
|
||||
if (isDirty(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)) {
|
||||
isDiscardable = true;
|
||||
if (AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)) {
|
||||
if (
|
||||
this.props.maxAutoPilotThroughput <= this.props.softAllowedMaximumThroughput &&
|
||||
AutoPilotUtils.isValidAutoPilotThroughput(this.props.maxAutoPilotThroughput)
|
||||
) {
|
||||
isSaveable = true;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +194,15 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
|
||||
let estimatedSpend: JSX.Element;
|
||||
|
||||
if (!this.props.isAutoPilotSelected) {
|
||||
if (this.props.isAutoPilotSelected) {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
userContext.portalEnv,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedManualSpendElement(
|
||||
// if migrating from autoscale to manual, we use the autoscale RUs value as that is what will be set...
|
||||
this.overrideWithAutoPilotSettings() ? this.props.maxAutoPilotThroughput : this.props.throughputBaseline,
|
||||
@@ -196,14 +211,6 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
multimaster,
|
||||
isDirty ? this.props.throughput : undefined
|
||||
);
|
||||
} else {
|
||||
estimatedSpend = this.getEstimatedAutoscaleSpendElement(
|
||||
this.props.maxAutoPilotThroughputBaseline,
|
||||
userContext.portalEnv,
|
||||
regions,
|
||||
multimaster,
|
||||
isDirty ? this.props.maxAutoPilotThroughput : undefined
|
||||
);
|
||||
}
|
||||
return estimatedSpend;
|
||||
};
|
||||
@@ -216,52 +223,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "minPerMonth",
|
||||
name: "Min Per Month",
|
||||
fieldName: "minPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "maxPerMonth",
|
||||
name: "Max Per Month",
|
||||
fieldName: "maxPerMonth",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)}
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newThroughputCostElement = (): JSX.Element => {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
@@ -269,37 +232,40 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
isMultimaster,
|
||||
true
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
minPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
maxPerMonth: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
true
|
||||
);
|
||||
const costElement = (): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice / 10)} min
|
||||
</Text>
|
||||
<Text style={{ width: "50%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)} max
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, true);
|
||||
};
|
||||
|
||||
private getEstimatedManualSpendElement = (
|
||||
@@ -310,122 +276,55 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
newThroughput?: number
|
||||
): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, false);
|
||||
const estimatedSpendingColumns: IColumn[] = [
|
||||
{
|
||||
key: "costType",
|
||||
name: "",
|
||||
fieldName: "costType",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "hourly",
|
||||
name: "Hourly",
|
||||
fieldName: "hourly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "daily",
|
||||
name: "Daily",
|
||||
fieldName: "daily",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "monthly",
|
||||
name: "Monthly",
|
||||
fieldName: "monthly",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
const newThroughputCostElement = (): JSX.Element => {
|
||||
const newPrices: PriceBreakdown = getRuPriceBreakdown(
|
||||
newThroughput,
|
||||
serverId,
|
||||
numberOfRegions,
|
||||
isMultimaster,
|
||||
false
|
||||
true
|
||||
);
|
||||
estimatedSpendingItems.unshift({
|
||||
costType: (
|
||||
<Text>
|
||||
<b>Updated Cost</b>
|
||||
</Text>
|
||||
),
|
||||
hourly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
daily: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
monthly: (
|
||||
<Text>
|
||||
<b>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Text style={{ fontWeight: 600 }}>Updated cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5, marginBottom: 10 }}>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return getEstimatedSpendingElement(
|
||||
estimatedSpendingColumns,
|
||||
estimatedSpendingItems,
|
||||
newThroughput ?? throughput,
|
||||
numberOfRegions,
|
||||
prices,
|
||||
false
|
||||
);
|
||||
};
|
||||
const costElement = (): JSX.Element => {
|
||||
const prices: PriceBreakdown = getRuPriceBreakdown(throughput, serverId, numberOfRegions, isMultimaster, true);
|
||||
return (
|
||||
<Stack {...checkBoxAndInputStackProps} style={{ marginTop: 15 }}>
|
||||
{newThroughput && newThroughputCostElement()}
|
||||
<Text style={{ fontWeight: 600 }}>Current cost per month</Text>
|
||||
<Stack horizontal style={{ marginTop: 5 }}>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.hourlyPrice)}/hr
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.dailyPrice)}/day
|
||||
</Text>
|
||||
<Text style={{ width: "33%" }}>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}/mo
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private getAutoPilotUsageCost = (): JSX.Element => {
|
||||
if (!this.props.maxAutoPilotThroughput) {
|
||||
return <></>;
|
||||
}
|
||||
return getAutoPilotV3SpendElement(
|
||||
this.props.maxAutoPilotThroughput,
|
||||
false /* isDatabaseThroughput */,
|
||||
!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>
|
||||
);
|
||||
return getEstimatedSpendingElement(costElement(), newThroughput ?? throughput, numberOfRegions, prices, false);
|
||||
};
|
||||
|
||||
private onAutoPilotThroughputChange = (
|
||||
@@ -512,7 +411,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
onChange={this.onChoiceGroupChange}
|
||||
required={this.props.showAsMandatory}
|
||||
ariaLabelledBy={labelId}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected)}
|
||||
styles={getChoiceGroupStyles(this.props.wasAutopilotOriginallySet, this.props.isAutoPilotSelected, true)}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
@@ -521,97 +420,266 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private onSpendAckChecked = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void =>
|
||||
this.setState({ spendAckChecked: checked });
|
||||
|
||||
private renderAutoPilotInput = (): JSX.Element => (
|
||||
<>
|
||||
<Text>
|
||||
Provision maximum RU/s required by this resource. Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`}
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
label="Max RU/s"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
errorMessage={this.props.throughputError}
|
||||
private getStorageCapacityTitle = (): JSX.Element => {
|
||||
const capacity: string = this.props.isFixed ? "Fixed" : "Unlimited";
|
||||
return (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Label>Storage capacity</Label>
|
||||
<Text>{capacity}</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
private thoughputRangeSeparatorStyles: Partial<ISeparatorStyles> = {
|
||||
root: [
|
||||
{
|
||||
selectors: {
|
||||
"::before": {
|
||||
backgroundColor: "rgb(200, 200, 200)",
|
||||
height: "3px",
|
||||
marginTop: "-1px",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
private currentThroughputValue = (): number => {
|
||||
return this.props.isAutoPilotSelected
|
||||
? this.props.maxAutoPilotThroughput
|
||||
: this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline
|
||||
: this.props.throughput;
|
||||
};
|
||||
|
||||
private getCurrentRuRange = (): "below" | "instant" | "delayed" | "requireSupport" => {
|
||||
if (this.currentThroughputValue() < this.props.minimum) {
|
||||
return "below";
|
||||
}
|
||||
if (
|
||||
this.currentThroughputValue() >= this.props.minimum &&
|
||||
this.currentThroughputValue() <= this.props.instantMaximumThroughput
|
||||
) {
|
||||
return "instant";
|
||||
}
|
||||
if (this.currentThroughputValue() > this.props.softAllowedMaximumThroughput) {
|
||||
return "requireSupport";
|
||||
}
|
||||
|
||||
return "delayed";
|
||||
};
|
||||
|
||||
private getRuThermometerStyles = (): Partial<IProgressIndicatorStyles> => ({
|
||||
progressBar: [
|
||||
{
|
||||
backgroundColor:
|
||||
this.getCurrentRuRange() === "instant"
|
||||
? "rgb(0, 120, 212)"
|
||||
: this.getCurrentRuRange() === "delayed"
|
||||
? "rgb(255 216 109)"
|
||||
: "rgb(251, 217, 203)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
private getRuThermometerPercentValue = (): number => {
|
||||
let percentValue: number;
|
||||
const currentRus = this.currentThroughputValue();
|
||||
|
||||
switch (this.getCurrentRuRange()) {
|
||||
case "below":
|
||||
percentValue = 0;
|
||||
break;
|
||||
case "instant": {
|
||||
const percentOfInstantRange: number = currentRus / this.props.instantMaximumThroughput;
|
||||
percentValue = percentOfInstantRange * 0.34;
|
||||
break;
|
||||
}
|
||||
case "delayed": {
|
||||
const adjustedMax = this.props.softAllowedMaximumThroughput - this.props.instantMaximumThroughput;
|
||||
const adjustedRus = currentRus - this.props.instantMaximumThroughput;
|
||||
const percentOfDelayedRange = adjustedRus / adjustedMax;
|
||||
const adjustedPercent = percentOfDelayedRange * 0.66;
|
||||
percentValue = adjustedPercent + 0.34;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// over maximum
|
||||
percentValue = 1;
|
||||
}
|
||||
return percentValue;
|
||||
};
|
||||
|
||||
private getRUThermometer = (): JSX.Element => (
|
||||
<Stack>
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "34%" }}>
|
||||
<span>{this.props.minimum.toLocaleString()}</span>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "66%" }}>
|
||||
<span style={{ float: "left", transform: "translateX(-50%)" }}>
|
||||
{this.props.instantMaximumThroughput.toLocaleString()}
|
||||
</span>
|
||||
<span style={{ float: "right" }}>{this.props.softAllowedMaximumThroughput.toLocaleString()}</span>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
<ProgressIndicator
|
||||
barHeight={20}
|
||||
percentComplete={this.getRuThermometerPercentValue()}
|
||||
styles={this.getRuThermometerStyles()}
|
||||
/>
|
||||
{!this.overrideWithProvisionedThroughputSettings() && this.getAutoPilotUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "34%", paddingRight: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>Instant</Separator>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "66%", paddingLeft: "5px" }}>
|
||||
<Separator styles={this.thoughputRangeSeparatorStyles}>4-6 hrs</Separator>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
private showThroughputWarning = (): boolean => {
|
||||
return (
|
||||
this.currentThroughputValue() > this.props.instantMaximumThroughput ||
|
||||
this.currentThroughputValue() < this.props.minimum
|
||||
);
|
||||
};
|
||||
|
||||
private getThroughputWarningMessageText = (): JSX.Element => {
|
||||
switch (this.getCurrentRuRange()) {
|
||||
case "below":
|
||||
return getUpdateThroughputBelowMinimumMessage(this.props.minimum);
|
||||
case "delayed":
|
||||
return getUpdateThroughputBeyondInstantLimitMessage(this.props.instantMaximumThroughput);
|
||||
case "requireSupport":
|
||||
return getUpdateThroughputBeyondSupportLimitMessage(
|
||||
this.props.instantMaximumThroughput,
|
||||
this.props.softAllowedMaximumThroughput
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
private getThroughputWarningMessageBar = (): JSX.Element => {
|
||||
const isSevereWarning: boolean =
|
||||
this.currentThroughputValue() > this.props.softAllowedMaximumThroughput ||
|
||||
this.currentThroughputValue() < this.props.minimum;
|
||||
return (
|
||||
<MessageBar messageBarType={isSevereWarning ? MessageBarType.severeWarning : MessageBarType.warning}>
|
||||
{this.getThroughputWarningMessageText()}
|
||||
</MessageBar>
|
||||
);
|
||||
};
|
||||
|
||||
private getThroughputTextField = (): JSX.Element => (
|
||||
<>
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<TextField
|
||||
label="Maximum RU/s required by this resource"
|
||||
required
|
||||
type="number"
|
||||
id="autopilotInput"
|
||||
key="auto pilot throughput input"
|
||||
styles={getTextFieldStyles(this.props.maxAutoPilotThroughput, this.props.maxAutoPilotThroughputBaseline)}
|
||||
disabled={this.overrideWithProvisionedThroughputSettings()}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
value={this.overrideWithProvisionedThroughputSettings() ? "" : this.props.maxAutoPilotThroughput?.toString()}
|
||||
onChange={this.onAutoPilotThroughputChange}
|
||||
min={autoPilotThroughput1K}
|
||||
onGetErrorMessage={(value: string) => {
|
||||
const sanitizedValue = getSanitizedInputValue(value);
|
||||
return sanitizedValue % 1000
|
||||
? "Throughput value must be in increments of 1000"
|
||||
: this.props.throughputError;
|
||||
}}
|
||||
validateOnLoad={false}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
: this.props.throughput?.toString()
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
</>
|
||||
);
|
||||
|
||||
private renderThroughputInput = (): JSX.Element => (
|
||||
<Stack {...titleAndInputStackProps}>
|
||||
<Text>
|
||||
Estimate your required throughput with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
id="throughputInput"
|
||||
key="provisioned throughput input"
|
||||
styles={getTextFieldStyles(this.props.throughput, this.props.throughputBaseline)}
|
||||
disabled={this.overrideWithAutoPilotSettings()}
|
||||
step={this.step}
|
||||
value={
|
||||
this.overrideWithAutoPilotSettings()
|
||||
? this.props.maxAutoPilotThroughputBaseline?.toString()
|
||||
: this.props.throughput?.toString()
|
||||
}
|
||||
onChange={this.onThroughputChange}
|
||||
min={this.props.minimum}
|
||||
errorMessage={this.props.throughputError}
|
||||
/>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "InfoSolid", className: "messageBarInfoIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{this.props.getThroughputWarningMessage()}
|
||||
</MessageBar>
|
||||
)}
|
||||
{!this.props.isEmulator && this.getRequestUnitsUsageCost()}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{this.props.isFixed && <p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>}
|
||||
private renderThroughputComponent = (): JSX.Element => (
|
||||
<Stack horizontal>
|
||||
<Stack.Item style={{ width: "70%", maxWidth: "700px" }}>
|
||||
<Stack {...relaxedSpacingStackProps} style={{ paddingRight: "50px" }}>
|
||||
{this.getThroughputTextField()}
|
||||
{this.props.instantMaximumThroughput && (
|
||||
<Stack>
|
||||
{this.getRUThermometer()}
|
||||
{this.showThroughputWarning() && this.getThroughputWarningMessageBar()}
|
||||
</Stack>
|
||||
)}
|
||||
{this.props.isAutoPilotSelected ? (
|
||||
<Text style={{ marginTop: "40px" }}>
|
||||
Based on usage, your {this.props.collectionName ? "container" : "database"} throughput will scale from{" "}
|
||||
<b>
|
||||
{AutoPilotUtils.getMinRUsBasedOnUserInput(this.props.maxAutoPilotThroughput)} RU/s (10% of max RU/s) -{" "}
|
||||
{this.props.maxAutoPilotThroughput} RU/s
|
||||
</b>
|
||||
<br />
|
||||
</Text>
|
||||
) : (
|
||||
<>
|
||||
{this.state.exceedFreeTierThroughput && (
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
style={{ marginTop: "40px" }}
|
||||
>
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!this.overrideWithProvisionedThroughputSettings() && (
|
||||
<Text>
|
||||
Estimate your required RU/s with
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/">
|
||||
{` capacity calculator`} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{this.minRUperGBSurvey()}
|
||||
{this.props.spendAckVisible && (
|
||||
<Checkbox
|
||||
id="spendAckCheckBox"
|
||||
styles={noLeftPaddingCheckBoxStyle}
|
||||
label={this.props.spendAckText}
|
||||
checked={this.state.spendAckChecked}
|
||||
onChange={this.onSpendAckChecked}
|
||||
/>
|
||||
)}
|
||||
{this.props.isFixed && (
|
||||
<p>When using a collection with fixed storage capacity, you can set up to 10,000 RU/s.</p>
|
||||
)}
|
||||
{this.props.collectionName && (
|
||||
<Stack.Item style={{ marginTop: "40px" }}>{this.getStorageCapacityTitle()}</Stack.Item>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ width: "30%", maxWidth: "300px" }}>
|
||||
{!this.props.isEmulator ? this.getRequestUnitsUsageCost() : <></>}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -624,7 +692,10 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
return (
|
||||
<>
|
||||
{warningMessage && (
|
||||
<MessageBar messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}>
|
||||
<MessageBar
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
role="alert"
|
||||
>
|
||||
{warningMessage}
|
||||
</MessageBar>
|
||||
)}
|
||||
@@ -638,7 +709,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
{this.renderWarningMessage()}
|
||||
{this.renderThroughputModeChoices()}
|
||||
|
||||
{this.props.isAutoPilotSelected ? this.renderAutoPilotInput() : this.renderThroughputInput()}
|
||||
{this.renderThroughputComponent()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,8 @@ exports[`ConflictResolutionComponent Path text field displayed 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -100,6 +102,8 @@ exports[`ConflictResolutionComponent Sproc text field displayed 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
|
||||
@@ -39,7 +39,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
canExceedMaximumValue={true}
|
||||
collectionName="test"
|
||||
databaseName="test"
|
||||
getThroughputWarningMessage={[Function]}
|
||||
isAutoPilotSelected={false}
|
||||
isEmulator={false}
|
||||
isEnabled={true}
|
||||
@@ -60,20 +59,6 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
usageSizeInKB={100}
|
||||
wasAutopilotOriginallySet={true}
|
||||
/>
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledLabelBase>
|
||||
Storage capacity
|
||||
</StyledLabelBase>
|
||||
<Text>
|
||||
Unlimited
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
@@ -40,6 +40,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -106,6 +108,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -168,6 +172,8 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -218,7 +224,10 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -264,6 +273,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -330,6 +341,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -382,6 +395,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -445,6 +460,8 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -495,7 +512,10 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -541,6 +561,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -607,6 +629,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -659,6 +683,8 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -734,7 +760,10 @@ exports[`SubSettingsComponent changeFeedPolicy hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -780,6 +809,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -846,6 +877,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -898,6 +931,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -986,6 +1021,8 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1036,7 +1073,10 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
@@ -1082,6 +1122,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": undefined,
|
||||
@@ -1123,6 +1165,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1175,6 +1219,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1263,6 +1309,8 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
Object {
|
||||
"flexContainer": Array [
|
||||
Object {
|
||||
"columnGap": "default",
|
||||
"display": "default",
|
||||
"selectors": Object {
|
||||
".ms-ChoiceField-field.is-checked::after": Object {
|
||||
"borderColor": "",
|
||||
@@ -1313,7 +1361,10 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
<Text>
|
||||
Large
|
||||
partition key
|
||||
has been enabled
|
||||
has been enabled.
|
||||
</Text>
|
||||
<Text>
|
||||
Non-hierarchically partitioned container.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -35,6 +35,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
@@ -111,6 +112,7 @@ exports[`SettingsComponent renders 1`] = `
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
|
||||
@@ -2,154 +2,60 @@
|
||||
|
||||
exports[`SettingsUtils functions render 1`] = `
|
||||
<Fragment>
|
||||
<Text>
|
||||
Your
|
||||
container
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Text>
|
||||
Your
|
||||
database
|
||||
throughput will automatically scale from
|
||||
|
||||
<b>
|
||||
100
|
||||
RU/s (10% of max RU/s) -
|
||||
|
||||
1000
|
||||
RU/s
|
||||
</b>
|
||||
|
||||
based on usage.
|
||||
<br />
|
||||
</Text>
|
||||
<Text>
|
||||
After the first
|
||||
10
|
||||
GB of data stored, the max RU/s will be automatically upgraded based on the new storage value.
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-info"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
.
|
||||
</Text>
|
||||
<Stack
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": 600,
|
||||
},
|
||||
}
|
||||
}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledWithViewportComponent
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"fieldName": "costType",
|
||||
"isResizable": true,
|
||||
"key": "costType",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "hourly",
|
||||
"isResizable": true,
|
||||
"key": "hourly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Hourly",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "daily",
|
||||
"isResizable": true,
|
||||
"key": "daily",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Daily",
|
||||
},
|
||||
Object {
|
||||
"fieldName": "monthly",
|
||||
"isResizable": true,
|
||||
"key": "monthly",
|
||||
"maxWidth": 200,
|
||||
"minWidth": 100,
|
||||
"name": "Monthly",
|
||||
},
|
||||
]
|
||||
}
|
||||
disableSelectionZone={true}
|
||||
items={
|
||||
Array [
|
||||
Object {
|
||||
"costType": <Text>
|
||||
Current Cost
|
||||
</Text>,
|
||||
"daily": <Text>
|
||||
$ 24.48
|
||||
</Text>,
|
||||
"hourly": <Text>
|
||||
$ 1.02
|
||||
</Text>,
|
||||
"monthly": <Text>
|
||||
$ 744.6
|
||||
</Text>,
|
||||
},
|
||||
]
|
||||
}
|
||||
layoutMode={1}
|
||||
onRenderRow={[Function]}
|
||||
selectionMode={0}
|
||||
/>
|
||||
<Stack>
|
||||
<Text
|
||||
id="throughputSpendElement"
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
(
|
||||
regions:
|
||||
|
||||
2
|
||||
,
|
||||
1000
|
||||
RU/s,
|
||||
¥
|
||||
0.00051
|
||||
/RU)
|
||||
Cost estimate*
|
||||
</Text>
|
||||
<Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontWeight": 600,
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
How we calculate this
|
||||
</Text>
|
||||
<Stack
|
||||
id="throughputSpendElement"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 5,
|
||||
}
|
||||
}
|
||||
>
|
||||
<span>
|
||||
2
|
||||
region
|
||||
<span>
|
||||
s
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
1000
|
||||
RU/s
|
||||
</span>
|
||||
<span>
|
||||
¥
|
||||
0.00051
|
||||
/RU
|
||||
</span>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<em>
|
||||
*
|
||||
This cost is an estimate and may vary based on the regions where your account is deployed and potential discounts applied to your account
|
||||
</em>
|
||||
</Text>
|
||||
@@ -205,19 +111,6 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
>
|
||||
You have not saved the latest changes made to your indexing policy. Please click save to confirm the changes.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputBeyondLimitWarningMessage"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
You are about to request an increase in throughput beyond the pre-allocated capacity. The service will scale out and increase throughput for the selected container. This operation will take 1-3 business days to complete. You can track the status of this request in Notifications.
|
||||
</Text>
|
||||
<Text
|
||||
id="updateThroughputDelayedApplyWarningMessage"
|
||||
styles={
|
||||
|
||||
@@ -46,10 +46,13 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
||||
}
|
||||
|
||||
let className = "toggleSwitch";
|
||||
let ariaselected;
|
||||
if (index === this.props.currentTabIndex) {
|
||||
className += " selectedToggle";
|
||||
ariaselected = true;
|
||||
} else {
|
||||
className += " unselectedToggle";
|
||||
ariaselected = false;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -57,9 +60,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
||||
<AccessibleElement
|
||||
as="span"
|
||||
className={className}
|
||||
role="presentation"
|
||||
role="tab"
|
||||
onActivated={() => this.setActiveTab(index)}
|
||||
aria-label={`Select tab: ${tab.title}`}
|
||||
aria-selected={ariaselected}
|
||||
>
|
||||
{tab.title}
|
||||
</AccessibleElement>
|
||||
@@ -77,7 +81,11 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
||||
|
||||
return (
|
||||
<div className="tabComponentContainer">
|
||||
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
|
||||
{!this.props.hideHeader && (
|
||||
<div className="tabs tabSwitch" role="tablist">
|
||||
{this.renderTabTitles()}
|
||||
</div>
|
||||
)}
|
||||
<div className={className}>{currentTabContent.render()}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -23,12 +23,12 @@ describe("ThroughputInput Pane", () => {
|
||||
});
|
||||
|
||||
it("should switch mode properly", () => {
|
||||
wrapper.find('[aria-label="Manual mode"]').simulate("change");
|
||||
wrapper.find('[aria-label="Manual database throughput"]').simulate("change");
|
||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe(
|
||||
"Container throughput (400 - unlimited RU/s)"
|
||||
);
|
||||
|
||||
wrapper.find('[aria-label="Autoscale mode"]').simulate("change");
|
||||
wrapper.find('[aria-label="Autoscale database throughput"]').simulate("change");
|
||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,36 +185,48 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
|
||||
/>
|
||||
<span className="throughputInputRadioBtnLabel">Autoscale</span>
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
id="Autoscale-input"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale database throughput"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
|
||||
/>
|
||||
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
|
||||
Autoscale
|
||||
</label>
|
||||
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Manual mode"
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
/>
|
||||
<span className="throughputInputRadioBtnLabel">Manual</span>
|
||||
<input
|
||||
id="Manual-input"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Manual database throughput"
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
/>
|
||||
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
|
||||
Manual
|
||||
</label>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{isAutoscaleSelected && (
|
||||
<Stack className="throughputInputSpacing">
|
||||
<Text variant="small" aria-label="ruDescription">
|
||||
<Text variant="small" aria-label="capacity calculator of azure cosmos db">
|
||||
Estimate your required RU/s with{" "}
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="ruDescription">
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
>
|
||||
capacity calculator
|
||||
</Link>
|
||||
.
|
||||
@@ -238,7 +250,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel={`${isDatabase ? "Database" : getCollectionName()} max RU/s`}
|
||||
required={true}
|
||||
errorMessage={throughputError}
|
||||
/>
|
||||
|
||||
@@ -344,13 +344,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<StyledIconBase
|
||||
ariaLabel="Info"
|
||||
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
ariaLabel="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
@@ -630,7 +630,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<i
|
||||
aria-label="Info"
|
||||
aria-label="Set the throughput — Request Units per second (RU/s) — required for the workload. A read of a 1 KB document uses 1 RU. Select manual if you plan to scale RU/s yourself. Select autoscale to allow the system to scale RU/s based on usage."
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
@@ -654,40 +654,45 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
<div
|
||||
className="ms-Stack css-58"
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
<div
|
||||
key=".0:$.0"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="throughputInputRadioBtnLabel"
|
||||
key=".0:$.1"
|
||||
role="radiogroup"
|
||||
>
|
||||
Autoscale
|
||||
</span>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="throughputInputRadioBtnLabel"
|
||||
key=".0:$.3"
|
||||
>
|
||||
Manual
|
||||
</span>
|
||||
<input
|
||||
aria-label="Autoscale database throughput"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
id="Autoscale-input"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="throughputInputRadioBtnLabel"
|
||||
htmlFor="Autoscale-input"
|
||||
>
|
||||
Autoscale
|
||||
</label>
|
||||
<input
|
||||
aria-label="Manual database throughput"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
id="Manual-input"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<label
|
||||
className="throughputInputRadioBtnLabel"
|
||||
htmlFor="Manual-input"
|
||||
>
|
||||
Manual
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -697,23 +702,23 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="ms-Stack throughputInputSpacing css-59"
|
||||
>
|
||||
<Text
|
||||
aria-label="ruDescription"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
key=".0:$.0"
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
aria-label="ruDescription"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
className="css-54"
|
||||
>
|
||||
Estimate your required RU/s with
|
||||
|
||||
<StyledLinkBase
|
||||
aria-label="ruDescription"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
target="_blank"
|
||||
>
|
||||
<LinkBase
|
||||
aria-label="ruDescription"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
styles={[Function]}
|
||||
target="_blank"
|
||||
@@ -992,7 +997,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<a
|
||||
aria-label="ruDescription"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
className="ms-Link root-60"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
onClick={[Function]}
|
||||
@@ -1331,13 +1336,13 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
onMouseLeave={[Function]}
|
||||
>
|
||||
<StyledIconBase
|
||||
ariaLabel="Info"
|
||||
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
ariaLabel="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
@@ -1617,7 +1622,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
}
|
||||
>
|
||||
<i
|
||||
aria-label="Info"
|
||||
aria-label="Set the max RU/s to the highest RU/s you want your container to scale to. The container will scale between 10% of max RU/s to the max RU/s based on usage."
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
@@ -1635,7 +1640,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</Stack>
|
||||
<StyledTextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Container max RU/s"
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
key=".0:$.2"
|
||||
@@ -1658,7 +1663,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
value="4000"
|
||||
>
|
||||
<TextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Container max RU/s"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
@@ -1956,6 +1961,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Container max RU/s"
|
||||
className="ms-TextField-field field-64"
|
||||
id="autoscaleRUValueField"
|
||||
min={1000}
|
||||
|
||||
@@ -195,7 +195,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
</div>
|
||||
{node.children && (
|
||||
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}>
|
||||
<div className="nodeChildren" data-test={node.label}>
|
||||
<div className="nodeChildren" data-test={node.label} role="group">
|
||||
{TreeNodeComponent.getSortedChildren(node).map((childNode: TreeNode) => (
|
||||
<TreeNodeComponent
|
||||
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}
|
||||
|
||||
@@ -100,6 +100,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
role="group"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={3}
|
||||
@@ -251,6 +252,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
role="group"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={13}
|
||||
@@ -352,6 +354,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
role="group"
|
||||
/>
|
||||
</AnimateHeight>
|
||||
</div>
|
||||
@@ -465,6 +468,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
role="group"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={13}
|
||||
@@ -594,6 +598,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
|
||||
<div
|
||||
className="nodeChildren"
|
||||
data-test="label"
|
||||
role="group"
|
||||
>
|
||||
<TreeNodeComponent
|
||||
generation={3}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
.treeComponent {
|
||||
.nodeItem {
|
||||
&:focus {
|
||||
outline: 1px dashed @AccentMedium;
|
||||
outline: 2px @AccentMedium;
|
||||
}
|
||||
|
||||
.treeNodeHeader {
|
||||
|
||||
@@ -133,7 +133,6 @@ describe("ContainerSampleGenerator", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
// Rejects with error that contains experience
|
||||
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,10 +22,17 @@ export class ContainerSampleGenerator {
|
||||
/**
|
||||
* Factory function to load the json data file
|
||||
*/
|
||||
public static async createSampleGeneratorAsync(container: Explorer): Promise<ContainerSampleGenerator> {
|
||||
public static async createSampleGeneratorAsync(
|
||||
container: Explorer,
|
||||
isCopilot?: boolean
|
||||
): Promise<ContainerSampleGenerator> {
|
||||
const generator = new ContainerSampleGenerator(container);
|
||||
let dataFileContent: any;
|
||||
if (userContext.apiType === "Gremlin") {
|
||||
if (isCopilot) {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "queryCopilotSampleData" */ "../../../sampleData/queryCopilotSampleData.json"
|
||||
);
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
dataFileContent = await import(
|
||||
/* webpackChunkName: "gremlinSampleJsonData" */ "../../../sampleData/gremlinSampleData.json"
|
||||
);
|
||||
|
||||
@@ -93,7 +93,8 @@ export default class Explorer {
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
});
|
||||
this._isInitializingNotebooks = false;
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
|
||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
useNotebook.subscribe(
|
||||
() => this.refreshCommandBarButtons(),
|
||||
(state) => state.isNotebooksEnabledForAccount
|
||||
@@ -185,9 +186,7 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
this.refreshExplorer();
|
||||
}
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
@@ -353,7 +352,7 @@ export default class Explorer {
|
||||
(notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined))
|
||||
) {
|
||||
const provisionData: IProvisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
cosmosEndpoint: userContext?.databaseAccount?.properties?.documentEndpoint,
|
||||
poolId: PoolIdType.DefaultPoolId,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
@@ -578,7 +577,7 @@ export default class Explorer {
|
||||
try {
|
||||
await Promise.all(
|
||||
databasesToLoad.map(async (database: ViewModels.Database) => {
|
||||
await database.loadCollections();
|
||||
await database.loadCollections(true);
|
||||
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
|
||||
if (isNewDatabase) {
|
||||
database.expandDatabase();
|
||||
@@ -1058,6 +1057,10 @@ export default class Explorer {
|
||||
title = "Cassandra Shell";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Postgres:
|
||||
title = "PSQL Shell";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Terminal kind: ${kind} not supported");
|
||||
}
|
||||
@@ -1244,9 +1247,11 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
|
||||
@@ -729,6 +729,7 @@ export class D3ForceGraph implements GraphRenderer {
|
||||
var iconGroup = newNodes
|
||||
.append("g")
|
||||
.attr("class", "iconContainer")
|
||||
.attr("role", "group")
|
||||
.attr("tabindex", 0)
|
||||
.attr("aria-label", (d: D3Node) => {
|
||||
return this.retrieveNodeCaption(d);
|
||||
|
||||
@@ -6,9 +6,9 @@ import * as React from "react";
|
||||
import LoadGraphIcon from "../../../../images/LoadGraph.png";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import { queryDocuments } from "../../../Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "../../../Common/dataAccess/queryDocumentsPage";
|
||||
import { getErrorMessage } from "../../../Common/ErrorHandlingUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { InputProperty } from "../../../Contracts/ViewModels";
|
||||
@@ -182,7 +182,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
public static readonly WITHOUT_STEP_ARGS_MAX_CHARS = 10000; // maximums char size of the without() step parameter
|
||||
public static readonly ROOT_LIST_PAGE_SIZE = 100;
|
||||
private static readonly MAX_LATEST_QUERIES = 10;
|
||||
private static readonly MAX_RESULT_SIZE = 10000;
|
||||
// TODO This prevents loading too much data in GraphExplorer and d3. A better is to cap the size of the result, rather than the number of items in the array.
|
||||
private static readonly MAX_RESULT_SIZE = 100_000;
|
||||
|
||||
private static readonly TAB_INDEX_JSON = 0;
|
||||
private static readonly TAB_INDEX_GRAPH = 1;
|
||||
|
||||
@@ -40,6 +40,7 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
|
||||
{/* svg load more icon inlined as-is here: remove the style="fill:#374649;" so we can override it */}
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="graph"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
@@ -135,6 +136,7 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
|
||||
<g id="triangleRight">
|
||||
<svg
|
||||
role="img"
|
||||
aria-label="graph"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -31,7 +31,7 @@ export class GremlinClient {
|
||||
public client: GremlinSimpleClient;
|
||||
public pendingResults: Map<string, PendingResultData>; // public for testing purposes
|
||||
private maxResultSize: number;
|
||||
private static readonly PENDING_REQUEST_TIMEOUT_MS = 6 /* minutes */ * 60 /* seconds */ * 1000 /* ms */;
|
||||
private static readonly PENDING_REQUEST_TIMEOUT_MS = 1 /* hour */ * 3_600 /* seconds */ * 1_000 /* ms */;
|
||||
private static readonly TIMEOUT_ERROR_MSG = `Pending request timed out (${GremlinClient.PENDING_REQUEST_TIMEOUT_MS} ms)`;
|
||||
private static readonly LOG_AREA = "GremlinClient";
|
||||
|
||||
|
||||
@@ -17,19 +17,21 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
||||
<div className="middlePane">
|
||||
<div className="graphTitle">
|
||||
<span className="paneTitle">Graph</span>
|
||||
<span
|
||||
<button
|
||||
style={{ border: "none", background: "none" }}
|
||||
className="graphExpandCollapseBtn pull-right"
|
||||
onClick={this.props.toggleExpandGraph}
|
||||
role="button"
|
||||
aria-expanded={this.props.isTabsContentExpanded}
|
||||
aria-name="View graph in full screen"
|
||||
tabIndex={0}
|
||||
aria-label={
|
||||
this.props.isTabsContentExpanded ? "Collapse graph to minimized view" : "View graph in full screen"
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="maingraphContainer">
|
||||
<GraphVizComponent forceGraphParams={this.props.graphVizProps.forceGraphParams} />
|
||||
|
||||
@@ -107,11 +107,11 @@ describe("New Vertex Component", () => {
|
||||
it("should call onTypeChange method on type dropdown change", () => {
|
||||
const DOWN_ARROW = { keyCode: 40 };
|
||||
const onTypeChange = jest.fn();
|
||||
const dropdown = screen.getByRole("listbox");
|
||||
const dropdown = screen.getByRole("combobox");
|
||||
dropdown.onclick = onTypeChange();
|
||||
dropdown.onkeydown = onTypeChange();
|
||||
|
||||
fireEvent.keyDown(screen.getByRole("listbox"), DOWN_ARROW);
|
||||
fireEvent.keyDown(screen.getByRole("combobox"), DOWN_ARROW);
|
||||
fireEvent.click(screen.getByText(/number/));
|
||||
expect(onTypeChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -145,6 +145,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
type="text"
|
||||
id="propertyKeyNewVertexPane"
|
||||
componentRef={input}
|
||||
aria-required="true"
|
||||
placeholder="Key"
|
||||
autoComplete="off"
|
||||
aria-label={`Enter value for propery ${index + 1}`}
|
||||
@@ -152,6 +153,8 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => onKeyChange(event, index)}
|
||||
/>
|
||||
</div>
|
||||
<span className="mandatoryStar">* </span>
|
||||
|
||||
<div className="valueCol">
|
||||
<TextField
|
||||
className="edgeInput"
|
||||
@@ -165,7 +168,7 @@ export const NewVertexComponent: FunctionComponent<INewVertexComponentProps> = (
|
||||
</div>
|
||||
<div>
|
||||
<Dropdown
|
||||
role="listbox"
|
||||
role="combobox"
|
||||
placeholder="Select an option"
|
||||
defaultSelectedKey={data.values[0].type}
|
||||
style={{ width: 100 }}
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
capabilities: [{ name: "EnableMongo" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
@@ -38,6 +38,38 @@ describe("CommandBarComponentButtonFactory tests", () => {
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeDefined();
|
||||
});
|
||||
|
||||
it("Button should not be visible for Tables API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableTable" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Button should not be visible for Cassandra API", () => {
|
||||
updateUserContext({
|
||||
databaseAccount: {
|
||||
properties: {
|
||||
capabilities: [{ name: "EnableCassandra" }],
|
||||
},
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
const buttons = CommandBarComponentButtonFactory.createStaticCommandBarButtons(mockExplorer, selectedNodeState);
|
||||
const enableAzureSynapseLinkBtn = buttons.find(
|
||||
(button) => button.commandButtonLabel === enableAzureSynapseLinkBtnLabel
|
||||
);
|
||||
expect(enableAzureSynapseLinkBtn).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Enable notebook button", () => {
|
||||
|
||||
@@ -51,11 +51,13 @@ export function createStaticCommandBarButtons(
|
||||
const buttons: CommandButtonComponentProps[] = [];
|
||||
|
||||
buttons.push(newCollectionBtn);
|
||||
if (userContext.apiType !== "Tables" && userContext.apiType !== "Cassandra") {
|
||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||
|
||||
const addSynapseLink = createOpenSynapseLinkDialogButton(container);
|
||||
if (addSynapseLink) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(addSynapseLink);
|
||||
if (addSynapseLink) {
|
||||
buttons.push(createDivider());
|
||||
buttons.push(addSynapseLink);
|
||||
}
|
||||
}
|
||||
|
||||
if (userContext.apiType !== "Tables") {
|
||||
@@ -202,6 +204,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
|
||||
if (showOpenFullScreen) {
|
||||
const label = "Open Full Screen";
|
||||
const fullScreenButton: CommandButtonComponentProps = {
|
||||
id: "openFullScreenBtn",
|
||||
iconSrc: OpenInTabIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
@@ -523,6 +526,28 @@ function createOpenCassandraTerminalButton(container: Explorer): CommandButtonCo
|
||||
};
|
||||
}
|
||||
|
||||
function createOpenPsqlTerminalButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Open PSQL Shell";
|
||||
const disableButton =
|
||||
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
|
||||
return {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () => {
|
||||
if (useNotebook.getState().isNotebookEnabled) {
|
||||
container.openNotebookTerminal(ViewModels.TerminalKind.Postgres);
|
||||
}
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: disableButton,
|
||||
ariaLabel: label,
|
||||
tooltipText: !disableButton
|
||||
? ""
|
||||
: "This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.",
|
||||
};
|
||||
}
|
||||
|
||||
function createNotebookWorkspaceResetButton(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "Reset Workspace";
|
||||
return {
|
||||
@@ -587,16 +612,7 @@ function createStaticCommandBarButtonsForResourceToken(
|
||||
}
|
||||
|
||||
export function createPostgreButtons(container: Explorer): CommandButtonComponentProps[] {
|
||||
const postgreShellLabel = "Open PostgreSQL Shell";
|
||||
const openPostgreShellBtn = {
|
||||
iconSrc: HostedTerminalIcon,
|
||||
iconAlt: postgreShellLabel,
|
||||
onCommandClick: () => container.openNotebookTerminal(ViewModels.TerminalKind.Mongo),
|
||||
commandButtonLabel: postgreShellLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: postgreShellLabel,
|
||||
};
|
||||
const openPostgreShellBtn = createOpenPsqlTerminalButton(container);
|
||||
|
||||
return [openPostgreShellBtn];
|
||||
}
|
||||
|
||||
@@ -147,12 +147,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
<div className="notificationConsoleControls">
|
||||
<Dropdown
|
||||
label="Filter:"
|
||||
role="combobox"
|
||||
selectedKey={this.state.selectedFilter}
|
||||
options={NotificationConsoleComponent.FilterOptions}
|
||||
onChange={this.onFilterSelected.bind(this)}
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
aria-label={this.state.selectedFilter}
|
||||
/>
|
||||
<span className="consoleSplitter" />
|
||||
<span
|
||||
|
||||
@@ -112,8 +112,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
className="notificationConsoleControls"
|
||||
>
|
||||
<Dropdown
|
||||
aria-label="All"
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
label="Filter:"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
@@ -136,7 +134,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
role="combobox"
|
||||
selectedKey="All"
|
||||
/>
|
||||
<span
|
||||
@@ -278,8 +275,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
className="notificationConsoleControls"
|
||||
>
|
||||
<Dropdown
|
||||
aria-label="All"
|
||||
aria-labelledby="consoleFilterLabel"
|
||||
label="Filter:"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
@@ -302,7 +297,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
role="combobox"
|
||||
selectedKey="All"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as InMemoryContentProviderUtils from "./ContentProviders/InMemoryContentProviderUtils";
|
||||
|
||||
describe("fromContentUri", () => {
|
||||
it("fromContentUri should return valid result", () => {
|
||||
const contentUri = "memory://resource/path";
|
||||
const result = "resource";
|
||||
|
||||
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(result);
|
||||
});
|
||||
|
||||
it("fromContentUri should return undefined on invalid input", () => {
|
||||
const contentUri = "invalid";
|
||||
|
||||
expect(InMemoryContentProviderUtils.fromContentUri(contentUri)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("toContentUri should return valid result", () => {
|
||||
const path = "resource/path";
|
||||
const result = "memory://resource/path";
|
||||
|
||||
expect(InMemoryContentProviderUtils.toContentUri(path)).toEqual(result);
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,7 @@ export class NotebookContainerClient {
|
||||
private scheduleTimerId: NodeJS.Timeout;
|
||||
|
||||
constructor(private onConnectionLost: () => void) {
|
||||
this.phoenixClient = new PhoenixClient();
|
||||
this.phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
this.retryOptions = {
|
||||
retries: Notebook.retryAttempts,
|
||||
maxTimeout: Notebook.retryAttemptDelayMs,
|
||||
|
||||
@@ -124,8 +124,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
}
|
||||
|
||||
const firstWriteLocation =
|
||||
databaseAccount?.properties?.writeLocations &&
|
||||
databaseAccount?.properties?.writeLocations[0]?.locationName.toLowerCase();
|
||||
userContext.apiType === "Postgres"
|
||||
? databaseAccount?.location
|
||||
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
|
||||
const disallowedLocationsUri = `${configContext.BACKEND_ENDPOINT}/api/disallowedLocations`;
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
try {
|
||||
@@ -307,13 +308,16 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
let isPhoenixFeatures = false;
|
||||
|
||||
const isPublicInternetAllowed = isPublicInternetAccessAllowed();
|
||||
const phoenixClient = new PhoenixClient();
|
||||
const phoenixClient = new PhoenixClient(userContext?.databaseAccount?.id);
|
||||
const dbAccountAllowedInfo = await phoenixClient.getDbAccountAllowedStatus();
|
||||
|
||||
if (dbAccountAllowedInfo.status === HttpStatusCodes.OK) {
|
||||
if (dbAccountAllowedInfo?.type === PhoenixErrorType.PhoenixFlightFallback) {
|
||||
isPhoenixNotebooks = isPublicInternetAllowed && userContext.features.phoenixNotebooks === true;
|
||||
isPhoenixFeatures = isPublicInternetAllowed && userContext.features.phoenixFeatures === true;
|
||||
isPhoenixFeatures =
|
||||
isPublicInternetAllowed &&
|
||||
// phoenix needs to be enabled for Postgres accounts since the PSQL shell requires phoenix containers
|
||||
(userContext.features.phoenixFeatures === true || userContext.apiType === "Postgres");
|
||||
} else {
|
||||
isPhoenixNotebooks = isPhoenixFeatures = isPublicInternetAllowed;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
jest.mock("../hooks/useFullScreenURLs");
|
||||
import "@testing-library/jest-dom";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
||||
import { OpenFullScreen } from "./OpenFullScreen";
|
||||
|
||||
it("renders the correct URLs", () => {
|
||||
(useFullScreenURLs as jest.Mock).mockReturnValue({
|
||||
readWrite: "read and write url",
|
||||
read: "read only url",
|
||||
});
|
||||
|
||||
render(<OpenFullScreen />);
|
||||
expect(screen.getByLabelText("Read and Write")).toHaveValue("https://cosmos.azure.com/?key=read and write url");
|
||||
expect(screen.getByLabelText("Read Only")).toHaveValue("https://cosmos.azure.com/?key=read only url");
|
||||
expect(screen.getByText("Open")).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -1,66 +1,26 @@
|
||||
import { DefaultButton, PrimaryButton, Spinner, Stack, Text, TextField } from "@fluentui/react";
|
||||
import copyToClipboard from "clipboard-copy";
|
||||
import { PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { useFullScreenURLs } from "../hooks/useFullScreenURLs";
|
||||
|
||||
export const OpenFullScreen: React.FunctionComponent = () => {
|
||||
const [isReadUrlCopy, setIsReadUrlCopy] = React.useState<boolean>(false);
|
||||
const [isReadWriteUrlCopy, setIsReadWriteUrlCopy] = React.useState<boolean>(false);
|
||||
const result = useFullScreenURLs();
|
||||
if (!result) {
|
||||
return <Spinner label="Generating URLs..." ariaLive="assertive" labelPosition="right" />;
|
||||
}
|
||||
|
||||
const readWriteUrl = `https://cosmos.azure.com/?key=${result.readWrite}`;
|
||||
const readUrl = `https://cosmos.azure.com/?key=${result.read}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Text>
|
||||
Open this database account in a new browser tab with Cosmos DB Explorer. Or copy the read-write or read only
|
||||
access urls below to share with others. For security purposes, the URLs grant time-bound access to the
|
||||
account. When access expires, you can reconnect, using a valid connection string for the account.
|
||||
</Text>
|
||||
<TextField label="Read and Write" readOnly defaultValue={readWriteUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadWriteUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
copyToClipboard(readWriteUrl);
|
||||
setIsReadWriteUrlCopy(true);
|
||||
}}
|
||||
text={isReadWriteUrlCopy ? "Copied" : "Copy"}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open(readWriteUrl, "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
<div style={{ padding: "34px" }}>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Text>
|
||||
Open this database account in a new browser tab with Cosmos DB Explorer. You can connect using your
|
||||
Microsoft account or a connection string.
|
||||
</Text>
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open("https://cosmos.azure.com/", "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<TextField label="Read Only" readOnly defaultValue={readUrl} />
|
||||
<Stack horizontal tokens={{ childrenGap: 10 }}>
|
||||
<DefaultButton
|
||||
ariaLabel={isReadUrlCopy ? "Copied url" : "Copy"}
|
||||
onClick={() => {
|
||||
setIsReadUrlCopy(true);
|
||||
copyToClipboard(readUrl);
|
||||
}}
|
||||
text={isReadUrlCopy ? "Copied" : "Copy"}
|
||||
iconProps={{ iconName: "Copy" }}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
window.open(readUrl, "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ const SharedDatabaseDefault: DataModels.IndexingPolicy = {
|
||||
],
|
||||
};
|
||||
|
||||
const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
export const AllPropertiesIndexed: DataModels.IndexingPolicy = {
|
||||
indexingMode: "consistent",
|
||||
automatic: true,
|
||||
includedPaths: [
|
||||
@@ -89,9 +89,10 @@ export interface AddCollectionPanelState {
|
||||
enableIndexing: boolean;
|
||||
isSharded: boolean;
|
||||
partitionKey: string;
|
||||
subPartitionKeys: string[];
|
||||
enableDedicatedThroughput: boolean;
|
||||
createMongoWildCardIndex: boolean;
|
||||
useHashV2: boolean;
|
||||
useHashV1: boolean;
|
||||
enableAnalyticalStore: boolean;
|
||||
uniqueKeys: string[];
|
||||
errorMessage: string;
|
||||
@@ -121,9 +122,11 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
enableIndexing: true,
|
||||
isSharded: userContext.apiType !== "Tables",
|
||||
partitionKey: this.getPartitionKey(),
|
||||
subPartitionKeys: [],
|
||||
enableDedicatedThroughput: false,
|
||||
createMongoWildCardIndex: isCapabilityEnabled("EnableMongo"),
|
||||
useHashV2: false,
|
||||
createMongoWildCardIndex:
|
||||
isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport"),
|
||||
useHashV1: false,
|
||||
enableAnalyticalStore: false,
|
||||
uniqueKeys: [],
|
||||
errorMessage: "",
|
||||
@@ -259,37 +262,46 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()}.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`A database is analogous to a namespace. It is the unit of management for a set of ${getCollectionName(
|
||||
true
|
||||
).toLocaleLowerCase()}.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
aria-label="Create new database"
|
||||
aria-checked={this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="databaseCreateNew"
|
||||
tabIndex={0}
|
||||
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Create new</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.createNewDatabase}
|
||||
aria-label="Use existing database"
|
||||
aria-checked={!this.state.createNewDatabase}
|
||||
name="databaseType"
|
||||
type="radio"
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Use existing</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{this.state.createNewDatabase && (
|
||||
@@ -335,7 +347,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Throughput configured at the database level will be shared across all ${getCollectionName(
|
||||
true
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -383,7 +402,13 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
role="button"
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -466,7 +491,14 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||
}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={
|
||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||
}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -513,7 +545,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={this.getPartitionKeyTooltipText()}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -545,6 +582,77 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{userContext.apiType === "SQL" &&
|
||||
this.state.subPartitionKeys.map((subPartitionKey: string, index: number) => {
|
||||
return (
|
||||
<Stack style={{ marginBottom: 8 }} key={`uniqueKey${index}`} horizontal>
|
||||
<div
|
||||
style={{
|
||||
width: "20px",
|
||||
border: "solid",
|
||||
borderWidth: "0px 0px 1px 1px",
|
||||
marginRight: "5px",
|
||||
}}
|
||||
></div>
|
||||
<input
|
||||
type="text"
|
||||
id="addCollection-partitionKeyValue"
|
||||
key={`addCollection-partitionKeyValue_${index}`}
|
||||
aria-required
|
||||
required
|
||||
size={40}
|
||||
tabIndex={index > 0 ? 1 : 0}
|
||||
className="panelTextField"
|
||||
autoComplete="off"
|
||||
placeholder={this.getPartitionKeyPlaceHolder(index)}
|
||||
aria-label={this.getPartitionKeyName()}
|
||||
pattern={".*"}
|
||||
title={""}
|
||||
value={subPartitionKey}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const subPartitionKeys = [...this.state.subPartitionKeys];
|
||||
if (!this.state.subPartitionKeys[index] && !event.target.value.startsWith("/")) {
|
||||
subPartitionKeys[index] = "/" + event.target.value.trim();
|
||||
this.setState({ subPartitionKeys });
|
||||
} else {
|
||||
subPartitionKeys[index] = event.target.value.trim();
|
||||
this.setState({ subPartitionKeys });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Delete" }}
|
||||
style={{ height: 27 }}
|
||||
onClick={() => {
|
||||
const subPartitionKeys = this.state.subPartitionKeys.filter((uniqueKey, j) => index !== j);
|
||||
this.setState({ subPartitionKeys });
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<DefaultButton
|
||||
styles={{ root: { padding: 0, width: 200, height: 30 }, label: { fontSize: 12 } }}
|
||||
hidden={this.state.useHashV1}
|
||||
disabled={this.state.subPartitionKeys.length >= Constants.BackendDefaults.maxNumMultiHashPartition}
|
||||
onClick={() => this.setState({ subPartitionKeys: [...this.state.subPartitionKeys, ""] })}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
</DefaultButton>
|
||||
{this.state.subPartitionKeys.length > 0 && (
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> This feature allows you to
|
||||
partition your data with up to three levels of keys for better data distribution. Requires .NET
|
||||
V3, Java V4 SDK, or preview JavaScript V3 SDK.{" "}
|
||||
<Link href="https://aka.ms/cosmos-hierarchical-partitioning" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
@@ -571,7 +679,17 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={`You can optionally provision dedicated throughput for a ${getCollectionName().toLocaleLowerCase()} within a database that has throughput
|
||||
provisioned. This dedicated throughput amount will not be shared with other ${getCollectionName(
|
||||
true
|
||||
).toLocaleLowerCase()} in the database and
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -602,11 +720,18 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
</Text>
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="Unique keys provide developers with the ability to add a layer of data integrity to their database. By
|
||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
||||
per partition key."
|
||||
content={
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel={
|
||||
"Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
}
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -669,40 +794,48 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without
|
||||
impacting the performance of transactional workloads."
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="enableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Enable analytical store"
|
||||
aria-checked={this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="enableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">On</span>
|
||||
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="disableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={!this.state.enableAnalyticalStore}
|
||||
disabled={!this.isSynapseLinkEnabled()}
|
||||
aria-label="Disable analytical store"
|
||||
aria-checked={!this.state.enableAnalyticalStore}
|
||||
name="analyticalStore"
|
||||
type="radio"
|
||||
role="radio"
|
||||
id="disableAnalyticalStoreBtn"
|
||||
tabIndex={0}
|
||||
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
|
||||
/>
|
||||
<span className="panelRadioBtnLabel">Off</span>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
{!this.isSynapseLinkEnabled() && (
|
||||
@@ -735,7 +868,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}}
|
||||
>
|
||||
<Stack className="panelGroupSpacing" id="collapsibleSectionContent">
|
||||
{isCapabilityEnabled("EnableMongo") && (
|
||||
{isCapabilityEnabled("EnableMongo") && !isCapabilityEnabled("EnableMongo16MBDocumentSupport") && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Stack horizontal>
|
||||
<span className="mandatoryStar">* </span>
|
||||
@@ -746,7 +879,12 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
<Icon
|
||||
iconName="Info"
|
||||
className="panelInfoIcon"
|
||||
tabIndex={0}
|
||||
ariaLabel="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||
/>
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -766,18 +904,29 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
)}
|
||||
|
||||
{userContext.apiType === "SQL" && (
|
||||
<Checkbox
|
||||
label="My partition key is larger than 101 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ useHashV2: isChecked })
|
||||
}
|
||||
/>
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Checkbox
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
checked={this.state.useHashV1}
|
||||
styles={{
|
||||
text: { fontSize: 12 },
|
||||
checkbox: { width: 12, height: 12 },
|
||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ useHashV1: isChecked, subPartitionKeys: [] })
|
||||
}
|
||||
/>
|
||||
<Text variant="small">
|
||||
<Icon iconName="InfoSolid" className="removeIcon" tabIndex={0} /> To ensure compatibility with
|
||||
older SDKs, the created container will use a legacy partitioning scheme that supports partition
|
||||
key values of size only up to 101 bytes. If this is enabled, you will not be able to use
|
||||
hierarchical partition keys.{" "}
|
||||
<Link href="https://aka.ms/cosmos-large-pk" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
@@ -832,12 +981,20 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return isLowerCase ? partitionKeyName.toLocaleLowerCase() : partitionKeyName;
|
||||
}
|
||||
|
||||
private getPartitionKeyPlaceHolder(): string {
|
||||
private getPartitionKeyPlaceHolder(index?: number): string {
|
||||
switch (userContext.apiType) {
|
||||
case "Mongo":
|
||||
return "e.g., address.zipCode";
|
||||
return "e.g., categoryId";
|
||||
case "Gremlin":
|
||||
return "e.g., /address";
|
||||
case "SQL":
|
||||
return `${
|
||||
index === undefined
|
||||
? "Required - first partition key e.g., /TenantId"
|
||||
: index === 0
|
||||
? "second partition key e.g., /UserId"
|
||||
: "third partition key e.g., /SessionId"
|
||||
}`;
|
||||
default:
|
||||
return "e.g., /address/zipCode";
|
||||
}
|
||||
@@ -959,7 +1116,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
return userContext.apiType === "SQL" ? "/pk" : "pk";
|
||||
}
|
||||
if (this.props.isQuickstart) {
|
||||
return userContext.apiType === "SQL" ? "/address" : "address";
|
||||
return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -1163,11 +1320,16 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
}
|
||||
|
||||
const uniqueKeyPolicy: DataModels.UniqueKeyPolicy = this.parseUniqueKeys();
|
||||
const partitionKeyVersion = this.state.useHashV2 ? 2 : undefined;
|
||||
const partitionKeyVersion = this.state.useHashV1 ? undefined : 2;
|
||||
const partitionKey: DataModels.PartitionKey = partitionKeyString
|
||||
? {
|
||||
paths: [partitionKeyString],
|
||||
kind: "Hash",
|
||||
paths: [
|
||||
partitionKeyString,
|
||||
...(userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0
|
||||
? this.state.subPartitionKeys
|
||||
: []),
|
||||
],
|
||||
kind: userContext.apiType === "SQL" && this.state.subPartitionKeys.length > 0 ? "MultiHash" : "Hash",
|
||||
version: partitionKeyVersion,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -89,8 +89,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
}
|
||||
} catch (error) {
|
||||
setLoadingFalse();
|
||||
setFormError(error);
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(errorMessage);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteDatabase,
|
||||
{
|
||||
|
||||
@@ -24,6 +24,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
|
||||
@@ -48,7 +48,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
)}
|
||||
</Text>
|
||||
{showErrorDetails && (
|
||||
<a className="paneErrorLink" role="link" onClick={expandConsole}>
|
||||
<a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
|
||||
More details
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -4,11 +4,11 @@ import React, { FunctionComponent, useState } from "react";
|
||||
import { Areas, SavedQueries } from "../../../Common/Constants";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
import { Query } from "../../../Contracts/DataModels";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||
import { useSidePanel } from "../../../hooks/useSidePanel";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NewQueryTab } from "../../Tabs/QueryTab/QueryTab";
|
||||
import { useDatabases } from "../../useDatabases";
|
||||
@@ -16,9 +16,13 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
||||
|
||||
interface SaveQueryPaneProps {
|
||||
explorer: Explorer;
|
||||
queryToSave?: string;
|
||||
}
|
||||
|
||||
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer }: SaveQueryPaneProps): JSX.Element => {
|
||||
export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({
|
||||
explorer,
|
||||
queryToSave,
|
||||
}: SaveQueryPaneProps): JSX.Element => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false);
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
@@ -36,7 +40,7 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
}
|
||||
|
||||
const queryTab = useTabs.getState().activeTab as NewQueryTab;
|
||||
const query: string = queryTab && queryTab.iTabAccessor.onSaveClickEvent();
|
||||
const query: string = queryToSave || queryTab?.iTabAccessor.onSaveClickEvent();
|
||||
|
||||
if (!queryName || queryName.length === 0) {
|
||||
setFormError("No query name specified");
|
||||
@@ -62,8 +66,8 @@ export const SaveQueryPane: FunctionComponent<SaveQueryPaneProps> = ({ explorer
|
||||
try {
|
||||
await explorer.queriesClient.saveQuery(queryParam);
|
||||
setLoadingFalse();
|
||||
queryTab.tabTitle(queryParam.queryName);
|
||||
queryTab.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
||||
queryTab?.tabTitle(queryParam.queryName);
|
||||
queryTab?.tabPath(`${queryTab.collection.databaseId}>${queryTab.collection.id()}>${queryParam.queryName}`);
|
||||
traceSuccess(
|
||||
Action.SaveQuery,
|
||||
{
|
||||
|
||||
@@ -21,6 +21,11 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
const [customItemPerPage, setCustomItemPerPage] = useState<number>(
|
||||
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
|
||||
);
|
||||
const [containerPaginationEnabled, setContainerPaginationEnabled] = useState<boolean>(
|
||||
LocalStorageUtility.hasItem(StorageKey.ContainerPaginationEnabled)
|
||||
? LocalStorageUtility.getEntryString(StorageKey.ContainerPaginationEnabled) === "true"
|
||||
: false
|
||||
);
|
||||
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
|
||||
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
|
||||
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
|
||||
@@ -50,6 +55,7 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
|
||||
);
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
|
||||
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
|
||||
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
|
||||
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
|
||||
|
||||
@@ -185,6 +191,25 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
<div className="settingsSectionLabel">
|
||||
Enable container pagination
|
||||
<InfoTooltip>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<Checkbox
|
||||
styles={{
|
||||
label: { padding: 0 },
|
||||
}}
|
||||
className="padding"
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={containerPaginationEnabled}
|
||||
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{shouldShowCrossPartitionOption && (
|
||||
<div className="settingsSection">
|
||||
<div className="settingsSectionPart">
|
||||
|
||||
@@ -97,6 +97,35 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<div
|
||||
className="settingsSectionLabel"
|
||||
>
|
||||
Enable container pagination
|
||||
<InfoTooltip>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
@@ -182,6 +211,35 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
<div
|
||||
className="paneMainContent"
|
||||
>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
<div
|
||||
className="settingsSectionPart"
|
||||
>
|
||||
<div
|
||||
className="settingsSectionLabel"
|
||||
>
|
||||
Enable container pagination
|
||||
<InfoTooltip>
|
||||
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
|
||||
</InfoTooltip>
|
||||
</div>
|
||||
<StyledCheckboxBase
|
||||
ariaLabel="Enable container pagination"
|
||||
checked={false}
|
||||
className="padding"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"padding": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="settingsSection"
|
||||
>
|
||||
|
||||
@@ -14,6 +14,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
||||
"onRefreshDatabasesKeyPress": [Function],
|
||||
"onRefreshResourcesClick": [Function],
|
||||
"phoenixClient": PhoenixClient {
|
||||
"armResourceId": undefined,
|
||||
"retryOptions": Object {
|
||||
"maxTimeout": 5000,
|
||||
"minTimeout": 5000,
|
||||
|
||||
@@ -242,6 +242,11 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
submitButtonText: getButtonLabel(userContext.apiType),
|
||||
onSubmit,
|
||||
};
|
||||
const handlekeypressaddentity = (event: React.KeyboardEvent<HTMLElement>) => {
|
||||
if (event.key === "Enter" || event.key === "Space") {
|
||||
addNewEntity();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
@@ -284,7 +289,13 @@ export const AddTableEntityPanel: FunctionComponent<AddTableEntityPanelProps> =
|
||||
);
|
||||
})}
|
||||
{userContext.apiType !== "Cassandra" && (
|
||||
<Stack horizontal onClick={addNewEntity} className="addButtonEntiy">
|
||||
<Stack
|
||||
horizontal
|
||||
onClick={addNewEntity}
|
||||
className="addButtonEntiy"
|
||||
tabIndex={0}
|
||||
onKeyPress={handlekeypressaddentity}
|
||||
>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add Entity" />
|
||||
<Text className="addNewParamStyle">{getAddButtonLabel(userContext.apiType)}</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -29,10 +29,14 @@ exports[`Excute Add Table Entity Pane should render Default properly 1`] = `
|
||||
className="addButtonEntiy"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<div
|
||||
className="ms-Stack addButtonEntiy css-53"
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
tabIndex={0}
|
||||
>
|
||||
<StyledImageBase
|
||||
alt="Add Entity"
|
||||
|
||||
@@ -31,6 +31,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="A database is analogous to a namespace. It is the unit of management for a set of containers."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -41,39 +42,43 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Create new database"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
id="databaseCreateNew"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
<div
|
||||
role="radiogroup"
|
||||
>
|
||||
Create new
|
||||
</span>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Use existing database"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Use existing
|
||||
</span>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Create new database"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
id="databaseCreateNew"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Create new
|
||||
</span>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Use existing database"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
name="databaseType"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Use existing
|
||||
</span>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
@@ -124,6 +129,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Throughput configured at the database level will be shared across all containers within the database."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -163,8 +169,10 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
@@ -206,6 +214,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="The partition key is used to automatically distribute data across partitions for scalability. Choose a property in your JSON document that has a wide range of values and evenly distributes request volume. For small read-heavy workloads or write-heavy workloads of any size, id is often a good choice."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -223,13 +232,36 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
id="addCollection-partitionKeyValue"
|
||||
onChange={[Function]}
|
||||
pattern=".*"
|
||||
placeholder="e.g., /address/zipCode"
|
||||
placeholder="Required - first partition key e.g., /TenantId"
|
||||
required={true}
|
||||
size={40}
|
||||
title=""
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<CustomizedDefaultButton
|
||||
disabled={false}
|
||||
hidden={false}
|
||||
onClick={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"label": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
"root": Object {
|
||||
"height": 30,
|
||||
"padding": 0,
|
||||
"width": 200,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Add hierarchical partition key
|
||||
</CustomizedDefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack
|
||||
@@ -246,6 +278,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Unique keys provide developers with the ability to add a layer of data integrity to their database. By creating a unique key policy when a container is created, you ensure the uniqueness of one or more values per partition key."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -303,6 +336,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
directionalHint={4}
|
||||
>
|
||||
<Icon
|
||||
ariaLabel="Enable analytical store capability to perform near real-time analytics on your operational data, without impacting the performance of transactional workloads."
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
@@ -313,42 +347,46 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Enable analytical store"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="enableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
<div
|
||||
role="radiogroup"
|
||||
>
|
||||
On
|
||||
</span>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Disable analytical store"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="disableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Off
|
||||
</span>
|
||||
<input
|
||||
aria-checked={false}
|
||||
aria-label="Enable analytical store"
|
||||
checked={false}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="enableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
On
|
||||
</span>
|
||||
<input
|
||||
aria-checked={true}
|
||||
aria-label="Disable analytical store"
|
||||
checked={true}
|
||||
className="panelRadioBtn"
|
||||
disabled={true}
|
||||
id="disableAnalyticalStoreBtn"
|
||||
name="analyticalStore"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
className="panelRadioBtnLabel"
|
||||
>
|
||||
Off
|
||||
</span>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
@@ -396,26 +434,49 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
className="panelGroupSpacing"
|
||||
id="collapsibleSectionContent"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
label="My partition key is larger than 101 bytes"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"checkbox": Object {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"alignItems": "center",
|
||||
"padding": 0,
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
"checkbox": Object {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
},
|
||||
"label": Object {
|
||||
"alignItems": "center",
|
||||
"padding": 0,
|
||||
"whiteSpace": "break-spaces",
|
||||
"wordWrap": "break-word",
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
<Text
|
||||
variant="small"
|
||||
>
|
||||
<Icon
|
||||
className="removeIcon"
|
||||
iconName="InfoSolid"
|
||||
tabIndex={0}
|
||||
/>
|
||||
To ensure compatibility with older SDKs, the created container will use a legacy partitioning scheme that supports partition key values of size only up to 101 bytes. If this is enabled, you will not be able to use hierarchical partition keys.
|
||||
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-large-pk"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</CollapsibleSectionComponent>
|
||||
</div>
|
||||
|
||||
11
src/Explorer/QueryCopilot/CopilotCarousel.test.tsx
Normal file
11
src/Explorer/QueryCopilot/CopilotCarousel.test.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import { QueryCopilotCarousel } from "./CopilotCarousel";
|
||||
|
||||
describe("Query Copilot Carousel snapshot test", () => {
|
||||
it("should render when isOpen is true", () => {
|
||||
const wrapper = shallow(<QueryCopilotCarousel isOpen={true} explorer={new Explorer()} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
277
src/Explorer/QueryCopilot/CopilotCarousel.tsx
Normal file
277
src/Explorer/QueryCopilot/CopilotCarousel.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
ISeparatorStyles,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
Modal,
|
||||
PrimaryButton,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { QueryCopilotSampleDatabaseId, StyleConstants } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
import { createCollection } from "Common/dataAccess/createCollection";
|
||||
import * as DataModels from "Contracts/DataModels";
|
||||
import { ContainerSampleGenerator } from "Explorer/DataSamples/ContainerSampleGenerator";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { AllPropertiesIndexed } from "Explorer/Panes/AddCollectionPanel";
|
||||
import { PromptCard } from "Explorer/QueryCopilot/PromptCard";
|
||||
import { useDatabases } from "Explorer/useDatabases";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React, { useState } from "react";
|
||||
import YoutubePlaceholder from "../../../images/YoutubePlaceholder.svg";
|
||||
|
||||
interface QueryCopilotCarouselProps {
|
||||
isOpen: boolean;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
"::before": {
|
||||
background: StyleConstants.BaseMedium,
|
||||
},
|
||||
},
|
||||
padding: "16px 0",
|
||||
height: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const QueryCopilotCarousel: React.FC<QueryCopilotCarouselProps> = ({
|
||||
isOpen,
|
||||
explorer,
|
||||
}: QueryCopilotCarouselProps): JSX.Element => {
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [isCreatingDatabase, setIsCreatingDatabase] = useState<boolean>(false);
|
||||
const [spinnerText, setSpinnerText] = useState<string>("");
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<number>(1);
|
||||
|
||||
const getHeaderText = (): string => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return "What exactly is copilot?";
|
||||
case 2:
|
||||
return "Setting up your Sample database";
|
||||
case 3:
|
||||
return "Sample prompts to help you";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getQueryCopilotInitialInput = (): string => {
|
||||
switch (selectedPrompt) {
|
||||
case 1:
|
||||
return "Write a query to return all records in this table";
|
||||
case 2:
|
||||
return "Write a query to return all records in this table created in the last thirty days";
|
||||
case 3:
|
||||
return 'Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"';
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const createSampleDatabase = async (): Promise<void> => {
|
||||
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||
if (database) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsCreatingDatabase(true);
|
||||
setSpinnerText("Setting up your database...");
|
||||
const params: DataModels.CreateCollectionParams = {
|
||||
createNewDatabase: true,
|
||||
collectionId: "SampleContainer",
|
||||
databaseId: QueryCopilotSampleDatabaseId,
|
||||
databaseLevelThroughput: true,
|
||||
autoPilotMaxThroughput: 1000,
|
||||
offerThroughput: undefined,
|
||||
indexingPolicy: AllPropertiesIndexed,
|
||||
partitionKey: {
|
||||
paths: ["/categoryId"],
|
||||
kind: "Hash",
|
||||
version: 2,
|
||||
},
|
||||
};
|
||||
await createCollection(params);
|
||||
await explorer.refreshAllDatabases();
|
||||
const database = useDatabases.getState().findDatabaseWithId(QueryCopilotSampleDatabaseId);
|
||||
await database.loadCollections();
|
||||
const collection = database.findCollectionWithId("SampleContainer");
|
||||
|
||||
setSpinnerText("Adding sample data set...");
|
||||
const sampleGenerator = await ContainerSampleGenerator.createSampleGeneratorAsync(explorer, true);
|
||||
await sampleGenerator.populateContainerAsync(collection);
|
||||
await database.expandDatabase();
|
||||
collection.expandCollection();
|
||||
useDatabases.getState().updateDatabase(database);
|
||||
} catch (error) {
|
||||
//TODO: show error in UI
|
||||
handleError(error, "QueryCopilotCreateSampleDB");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsCreatingDatabase(false);
|
||||
setSpinnerText("");
|
||||
}
|
||||
};
|
||||
|
||||
const getContent = (): JSX.Element => {
|
||||
switch (page) {
|
||||
case 1:
|
||||
return (
|
||||
<Stack style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 13 }}>
|
||||
A couple of lines about copilot and the background about it. The idea is to have some text to give context
|
||||
to the user.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>How do you use copilot</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
To generate queries , just describe the query you want and copilot will generate the query for you.Watch
|
||||
this video to learn more about how to use copilot.
|
||||
</Text>
|
||||
<Image src={YoutubePlaceholder} style={{ margin: "16px auto" }} />
|
||||
<Text style={{ fontSize: 14, fontWeight: 600 }}>What is copilot good at</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
A couple of lines about what copilot can do and its capablites with a link to{" "}
|
||||
<Link href="" target="_blank">
|
||||
documentation
|
||||
</Link>{" "}
|
||||
if possible.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>What are its limitations</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
A couple of lines about what copilot cant do and its limitations.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Link to documentation
|
||||
</Link>
|
||||
</Text>
|
||||
<Text style={{ fontSize: 14, fontWeight: 600, marginTop: 16 }}>Disclaimer</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 8 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Stack style={{ marginTop: 8 }}>
|
||||
<Text style={{ fontSize: 13 }}>
|
||||
Before you get started, we need to configure your sample database for you. Here is a summary of the
|
||||
database being created for your reference. Configuration values can be updated using the settings icon in
|
||||
the query builder.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 24 }}>Database Id</Text>
|
||||
<Text style={{ fontSize: 13 }}>CopilotSampleDb</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database throughput (autoscale)</Text>
|
||||
<Text style={{ fontSize: 13 }}>Autoscale</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Database Max RU/s</Text>
|
||||
<Text>1000</Text>
|
||||
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||
Your database throughput will automatically scale from{" "}
|
||||
<strong>100 RU/s (10% of max RU/s) - 1000 RU/s</strong> based on usage.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 10, marginTop: 8 }}>
|
||||
Estimated monthly cost (USD): <strong>$8.76 - $87.60</strong> (1 region, 100 - 1000 RU/s, $0.00012/RU)
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Container Id</Text>
|
||||
<Text style={{ fontSize: 13 }}>SampleContainer</Text>
|
||||
<Text style={{ fontSize: 13, fontWeight: 600, marginTop: 16 }}>Partition key</Text>
|
||||
<Text style={{ fontSize: 13 }}>categoryId</Text>
|
||||
</Stack>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Stack>
|
||||
<Text>To help you get started, here are some sample prompts to get you started</Text>
|
||||
<Stack tokens={{ childrenGap: 12 }} style={{ marginTop: 16 }}>
|
||||
<PromptCard
|
||||
header="Write a query to return all records in this table"
|
||||
description="This is a basic query which returns all records in the table "
|
||||
onSelect={() => setSelectedPrompt(1)}
|
||||
isSelected={selectedPrompt === 1}
|
||||
/>
|
||||
<PromptCard
|
||||
header="Write a query to return all records in this table created in the last thirty days"
|
||||
description="This builds on the previous query which returns all records in the table which were inserted in the last thirty days. You can also modify this query to return records based upon creation date"
|
||||
onSelect={() => setSelectedPrompt(2)}
|
||||
isSelected={selectedPrompt === 2}
|
||||
/>
|
||||
<PromptCard
|
||||
header='Write a query to return all records in this table created in the last thirty days which also have the record owner as "Contoso"'
|
||||
description='This builds on the previous query which returns all records in the table which were inserted in the last thirty days but which has the record owner as "contoso"'
|
||||
onSelect={() => setSelectedPrompt(3)}
|
||||
isSelected={selectedPrompt === 3}
|
||||
/>
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 13, marginTop: 32 }}>
|
||||
Interested in learning more about how to write effective prompts. Please read this article for more
|
||||
information.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||
You can also access these prompts by selecting the Samples prompts button in the query builder page.
|
||||
</Text>
|
||||
<Text style={{ fontSize: 13, marginTop: 16 }}>
|
||||
Don't like any of the prompts? Just click Get Started and write your own prompt.
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal styles={{ main: { width: 880 } }} isOpen={isOpen && page < 4}>
|
||||
<Stack style={{ padding: 16 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text variant="xLarge">{getHeaderText()}</Text>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Cancel" }}
|
||||
onClick={() => useCarousel.getState().setShowCopilotCarousel(false)}
|
||||
/>
|
||||
</Stack>
|
||||
{getContent()}
|
||||
<Separator styles={separatorStyles} />
|
||||
<Stack horizontal horizontalAlign="start" verticalAlign="center">
|
||||
{page !== 1 && (
|
||||
<DefaultButton
|
||||
text="Previous"
|
||||
style={{ marginRight: 8 }}
|
||||
onClick={() => setPage(page - 1)}
|
||||
disabled={isCreatingDatabase}
|
||||
/>
|
||||
)}
|
||||
<PrimaryButton
|
||||
text={page === 3 ? "Get started" : "Next"}
|
||||
onClick={async () => {
|
||||
if (page === 3) {
|
||||
useCarousel.getState().setShowCopilotCarousel(false);
|
||||
useTabs.getState().setQueryCopilotTabInitialInput(getQueryCopilotInitialInput());
|
||||
useTabs.getState().openAndActivateReactTab(ReactTabKind.QueryCopilot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (page === 2) {
|
||||
await createSampleDatabase();
|
||||
}
|
||||
|
||||
setPage(page + 1);
|
||||
}}
|
||||
disabled={isCreatingDatabase}
|
||||
/>
|
||||
{isCreatingDatabase && <Spinner style={{ marginLeft: 8 }} />}
|
||||
{isCreatingDatabase && <Text style={{ marginLeft: 8, color: "#0078D4" }}>{spinnerText}</Text>}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
19
src/Explorer/QueryCopilot/PromptCard.test.tsx
Normal file
19
src/Explorer/QueryCopilot/PromptCard.test.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import { PromptCard } from "./PromptCard";
|
||||
|
||||
describe("Prompt card snapshot test", () => {
|
||||
it("should render properly if isSelected is true", () => {
|
||||
const wrapper = shallow(
|
||||
<PromptCard header="TestHeader" description="TestDescription" isSelected={true} onSelect={() => undefined} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should render properly if isSelected is false", () => {
|
||||
const wrapper = shallow(
|
||||
<PromptCard header="TestHeader" description="TestDescription" isSelected={false} onSelect={() => undefined} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
48
src/Explorer/QueryCopilot/PromptCard.tsx
Normal file
48
src/Explorer/QueryCopilot/PromptCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ChoiceGroup, Stack, Text } from "@fluentui/react";
|
||||
import React from "react";
|
||||
|
||||
interface PromptCardProps {
|
||||
header: string;
|
||||
description: string;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}
|
||||
|
||||
export const PromptCard: React.FC<PromptCardProps> = ({
|
||||
header,
|
||||
description,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: PromptCardProps): JSX.Element => {
|
||||
return (
|
||||
<Stack
|
||||
horizontal
|
||||
style={{
|
||||
padding: "16px 0 16px 16px ",
|
||||
boxSizing: "border-box",
|
||||
width: 650,
|
||||
height: 100,
|
||||
border: "1px solid #F3F2F1",
|
||||
boxShadow: "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
}}
|
||||
>
|
||||
<Stack.Item grow={1}>
|
||||
<Stack horizontal>
|
||||
<div>
|
||||
<Text style={{ fontSize: 13, color: "#00A2AD", background: "#F8FFF0" }}>Prompt</Text>
|
||||
</div>
|
||||
<Text style={{ fontSize: 13, marginLeft: 16 }}>{header}</Text>
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 10, marginTop: 16 }}>{description}</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item style={{ marginLeft: 16 }}>
|
||||
<ChoiceGroup
|
||||
styles={{ flexContainer: { width: 36 } }}
|
||||
options={[{ key: "selected", text: "" }]}
|
||||
selectedKey={isSelected ? "selected" : ""}
|
||||
onChange={onSelect}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
105
src/Explorer/QueryCopilot/QueryCopilotFeedbackModal.tsx
Normal file
105
src/Explorer/QueryCopilot/QueryCopilotFeedbackModal.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Checkbox,
|
||||
ChoiceGroup,
|
||||
DefaultButton,
|
||||
IconButton,
|
||||
Link,
|
||||
Modal,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import React from "react";
|
||||
|
||||
export const QueryCopilotFeedbackModal: React.FC = (): JSX.Element => {
|
||||
const {
|
||||
generatedQuery,
|
||||
userPrompt,
|
||||
likeQuery,
|
||||
showFeedbackModal,
|
||||
closeFeedbackModal,
|
||||
setHideFeedbackModalForLikedQueries,
|
||||
} = useQueryCopilot();
|
||||
const [isContactAllowed, setIsContactAllowed] = React.useState<boolean>(true);
|
||||
const [description, setDescription] = React.useState<string>("");
|
||||
const [doNotShowAgainChecked, setDoNotShowAgainChecked] = React.useState<boolean>(false);
|
||||
return (
|
||||
<Modal isOpen={showFeedbackModal}>
|
||||
<Stack style={{ padding: 24 }}>
|
||||
<Stack horizontal horizontalAlign="space-between">
|
||||
<Text style={{ fontSize: 20, fontWeight: 600, marginBottom: 20 }}>Send feedback to Microsoft</Text>
|
||||
<IconButton iconProps={{ iconName: "Cancel" }} onClick={() => closeFeedbackModal()} />
|
||||
</Stack>
|
||||
<Text style={{ fontSize: 14, marginBottom: 14 }}>Your feedback will help improve the experience.</Text>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Description"
|
||||
required
|
||||
placeholder="Provide more details"
|
||||
value={description}
|
||||
onChange={(_, newValue) => setDescription(newValue)}
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
<TextField
|
||||
styles={{ root: { marginBottom: 14 } }}
|
||||
label="Query generated"
|
||||
defaultValue={generatedQuery}
|
||||
readOnly
|
||||
/>
|
||||
<ChoiceGroup
|
||||
styles={{
|
||||
root: {
|
||||
marginBottom: 14,
|
||||
},
|
||||
flexContainer: {
|
||||
selectors: {
|
||||
".ms-ChoiceField-field::before": { marginTop: 4 },
|
||||
".ms-ChoiceField-field::after": { marginTop: 4 },
|
||||
".ms-ChoiceFieldLabel": { paddingLeft: 6 },
|
||||
},
|
||||
},
|
||||
}}
|
||||
label="May we contact you about your feedback?"
|
||||
options={[
|
||||
{ key: "yes", text: "Yes, you may contact me." },
|
||||
{ key: "no", text: "No, do not contact me." },
|
||||
]}
|
||||
selectedKey={isContactAllowed ? "yes" : "no"}
|
||||
onChange={(_, option) => setIsContactAllowed(option.key === "yes")}
|
||||
></ChoiceGroup>
|
||||
<Text style={{ fontSize: 12, marginBottom: 14 }}>
|
||||
By pressing submit, your feedback will be used to improve Microsoft products and services. IT admins for your
|
||||
organization will be able to view and manage your feedback data.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Privacy statement
|
||||
</Link>
|
||||
</Text>
|
||||
{likeQuery && (
|
||||
<Checkbox
|
||||
styles={{ label: { paddingLeft: 0 }, root: { marginBottom: 14 } }}
|
||||
label="Don't show me this next time"
|
||||
checked={doNotShowAgainChecked}
|
||||
onChange={(_, checked) => setDoNotShowAgainChecked(checked)}
|
||||
/>
|
||||
)}
|
||||
<Stack horizontal horizontalAlign="end">
|
||||
<PrimaryButton
|
||||
styles={{ root: { marginRight: 8 } }}
|
||||
onClick={() => {
|
||||
closeFeedbackModal();
|
||||
setHideFeedbackModalForLikedQueries(doNotShowAgainChecked);
|
||||
submitFeedback({ generatedQuery, likeQuery, description, userPrompt });
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</PrimaryButton>
|
||||
<DefaultButton onClick={() => closeFeedbackModal()}>Cancel</DefaultButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
13
src/Explorer/QueryCopilot/QueryCopilotTab.test.tsx
Normal file
13
src/Explorer/QueryCopilot/QueryCopilotTab.test.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import Explorer from "../Explorer";
|
||||
import { QueryCopilotTab } from "./QueryCopilotTab";
|
||||
|
||||
describe("Query copilot tab snapshot test", () => {
|
||||
it("should render with initial input", () => {
|
||||
const wrapper = shallow(
|
||||
<QueryCopilotTab initialInput="Write a query to return all records in this table" explorer={new Explorer()} />
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
275
src/Explorer/QueryCopilot/QueryCopilotTab.tsx
Normal file
275
src/Explorer/QueryCopilot/QueryCopilotTab.tsx
Normal file
@@ -0,0 +1,275 @@
|
||||
/* eslint-disable no-console */
|
||||
import { FeedOptions } from "@azure/cosmos";
|
||||
import {
|
||||
Callout,
|
||||
CommandBarButton,
|
||||
DirectionalHint,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
Separator,
|
||||
Spinner,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import {
|
||||
QueryCopilotSampleContainerId,
|
||||
QueryCopilotSampleContainerSchema,
|
||||
QueryCopilotSampleDatabaseId,
|
||||
} from "Common/Constants";
|
||||
import { getErrorMessage, handleError } from "Common/ErrorHandlingUtils";
|
||||
import { shouldEnableCrossPartitionKey } from "Common/HeadersUtility";
|
||||
import { MinimalQueryIterator } from "Common/IteratorUtilities";
|
||||
import { queryDocuments } from "Common/dataAccess/queryDocuments";
|
||||
import { queryDocumentsPage } from "Common/dataAccess/queryDocumentsPage";
|
||||
import { QueryResults } from "Contracts/ViewModels";
|
||||
import { CommandButtonComponentProps } from "Explorer/Controls/CommandButton/CommandButtonComponent";
|
||||
import { EditorReact } from "Explorer/Controls/Editor/EditorReact";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { SaveQueryPane } from "Explorer/Panes/SaveQueryPane/SaveQueryPane";
|
||||
import { submitFeedback } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||
import { QueryResultSection } from "Explorer/Tabs/QueryTab/QueryResultSection";
|
||||
import { queryPagesUntilContentPresent } from "Utils/QueryUtils";
|
||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||
import { useSidePanel } from "hooks/useSidePanel";
|
||||
import React, { useState } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import CopilotIcon from "../../../images/Copilot.svg";
|
||||
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
|
||||
import SaveQueryIcon from "../../../images/save-cosmos.svg";
|
||||
|
||||
interface QueryCopilotTabProps {
|
||||
initialInput: string;
|
||||
explorer: Explorer;
|
||||
}
|
||||
|
||||
interface GenerateSQLQueryResponse {
|
||||
apiVersion: string;
|
||||
sql: string;
|
||||
explanation: string;
|
||||
generateStart: string;
|
||||
generateEnd: string;
|
||||
}
|
||||
|
||||
export const QueryCopilotTab: React.FC<QueryCopilotTabProps> = ({
|
||||
initialInput,
|
||||
explorer,
|
||||
}: QueryCopilotTabProps): JSX.Element => {
|
||||
const hideFeedbackModalForLikedQueries = useQueryCopilot((state) => state.hideFeedbackModalForLikedQueries);
|
||||
const [userInput, setUserInput] = useState<string>(initialInput || "");
|
||||
const [generatedQuery, setGeneratedQuery] = useState<string>("");
|
||||
const [query, setQuery] = useState<string>("");
|
||||
const [selectedQuery, setSelectedQuery] = useState<string>("");
|
||||
const [isGeneratingQuery, setIsGeneratingQuery] = useState<boolean>(false);
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||
const [likeQuery, setLikeQuery] = useState<boolean>();
|
||||
const [showCallout, setShowCallout] = useState<boolean>(false);
|
||||
const [queryIterator, setQueryIterator] = useState<MinimalQueryIterator>();
|
||||
const [queryResults, setQueryResults] = useState<QueryResults>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
|
||||
const generateSQLQuery = async (): Promise<void> => {
|
||||
try {
|
||||
setIsGeneratingQuery(true);
|
||||
const payload = {
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
userPrompt: userInput,
|
||||
};
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/generateSQLQuery", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const generateSQLQueryResponse: GenerateSQLQueryResponse = await response?.json();
|
||||
if (generateSQLQueryResponse?.sql) {
|
||||
let query = `-- **Prompt:** ${userInput}\r\n`;
|
||||
if (generateSQLQueryResponse.explanation) {
|
||||
query += `-- **Explanation of query:** ${generateSQLQueryResponse.explanation}\r\n`;
|
||||
}
|
||||
query += generateSQLQueryResponse.sql;
|
||||
setQuery(query);
|
||||
setGeneratedQuery(generateSQLQueryResponse.sql);
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "executeNaturalLanguageQuery");
|
||||
throw error;
|
||||
} finally {
|
||||
setIsGeneratingQuery(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onExecuteQueryClick = async (): Promise<void> => {
|
||||
const queryToExecute = selectedQuery || query;
|
||||
const queryIterator = queryDocuments(QueryCopilotSampleDatabaseId, QueryCopilotSampleContainerId, queryToExecute, {
|
||||
enableCrossPartitionQuery: shouldEnableCrossPartitionKey(),
|
||||
} as FeedOptions);
|
||||
setQueryIterator(queryIterator);
|
||||
|
||||
setTimeout(async () => {
|
||||
await queryDocumentsPerPage(0, queryIterator);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const queryDocumentsPerPage = async (firstItemIndex: number, queryIterator: MinimalQueryIterator): Promise<void> => {
|
||||
try {
|
||||
setIsExecuting(true);
|
||||
const queryResults: QueryResults = await queryPagesUntilContentPresent(
|
||||
firstItemIndex,
|
||||
async (firstItemIndex: number) =>
|
||||
queryDocumentsPage(QueryCopilotSampleContainerId, queryIterator, firstItemIndex)
|
||||
);
|
||||
|
||||
setQueryResults(queryResults);
|
||||
setErrorMessage("");
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setErrorMessage(errorMessage);
|
||||
handleError(errorMessage, "executeQueryCopilotTab");
|
||||
} finally {
|
||||
setIsExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getCommandbarButtons = (): CommandButtonComponentProps[] => {
|
||||
const executeQueryBtnLabel = selectedQuery ? "Execute Selection" : "Execute Query";
|
||||
const executeQueryBtn = {
|
||||
iconSrc: ExecuteQueryIcon,
|
||||
iconAlt: executeQueryBtnLabel,
|
||||
onCommandClick: () => onExecuteQueryClick(),
|
||||
commandButtonLabel: executeQueryBtnLabel,
|
||||
ariaLabel: executeQueryBtnLabel,
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
const saveQueryBtn = {
|
||||
iconSrc: SaveQueryIcon,
|
||||
iconAlt: "Save Query",
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("Save Query", <SaveQueryPane explorer={explorer} queryToSave={query} />),
|
||||
commandButtonLabel: "Save Query",
|
||||
ariaLabel: "Save Query",
|
||||
hasPopup: false,
|
||||
};
|
||||
|
||||
return [executeQueryBtn, saveQueryBtn];
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
useCommandBar.getState().setContextButtons(getCommandbarButtons());
|
||||
}, [query, selectedQuery]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialInput) {
|
||||
generateSQLQuery();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack className="tab-pane" style={{ padding: 24, width: "100%", height: "100%" }}>
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<Image src={CopilotIcon} />
|
||||
<Text style={{ marginLeft: 8, fontWeight: 600, fontSize: 16 }}>Copilot</Text>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginTop: 16, width: "100%" }}>
|
||||
<TextField
|
||||
value={userInput}
|
||||
onChange={(_, newValue) => setUserInput(newValue)}
|
||||
style={{ lineHeight: 30 }}
|
||||
styles={{ root: { width: "90%" } }}
|
||||
disabled={isGeneratingQuery}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{ iconName: "Send" }}
|
||||
disabled={isGeneratingQuery}
|
||||
style={{ marginLeft: 8 }}
|
||||
onClick={() => generateSQLQuery()}
|
||||
/>
|
||||
{isGeneratingQuery && <Spinner style={{ marginLeft: 8 }} />}
|
||||
</Stack>
|
||||
<Text style={{ marginTop: 8, marginBottom: 24, fontSize: 12 }}>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.{" "}
|
||||
<Link href="" target="_blank">
|
||||
Read preview terms
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Stack style={{ backgroundColor: "#FFF8F0", padding: "2px 8px" }} horizontal verticalAlign="center">
|
||||
<Text style={{ fontWeight: 600, fontSize: 12 }}>Provide feedback on the query generated</Text>
|
||||
{showCallout && !hideFeedbackModalForLikedQueries && (
|
||||
<Callout
|
||||
style={{ padding: 8 }}
|
||||
target="#likeBtn"
|
||||
onDismiss={() => {
|
||||
setShowCallout(false);
|
||||
submitFeedback({ generatedQuery, likeQuery, description: "", userPrompt: userInput });
|
||||
}}
|
||||
directionalHint={DirectionalHint.topCenter}
|
||||
>
|
||||
<Text>
|
||||
Thank you. Need to give{" "}
|
||||
<Link
|
||||
onClick={() => {
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, true, userInput);
|
||||
}}
|
||||
>
|
||||
more feedback?
|
||||
</Link>
|
||||
</Text>
|
||||
</Callout>
|
||||
)}
|
||||
<IconButton
|
||||
id="likeBtn"
|
||||
style={{ marginLeft: 20 }}
|
||||
iconProps={{ iconName: likeQuery === true ? "LikeSolid" : "Like" }}
|
||||
onClick={() => {
|
||||
setLikeQuery(true);
|
||||
setShowCallout(true);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
style={{ margin: "0 10px" }}
|
||||
iconProps={{ iconName: likeQuery === false ? "DislikeSolid" : "Dislike" }}
|
||||
onClick={() => {
|
||||
setLikeQuery(false);
|
||||
setShowCallout(false);
|
||||
useQueryCopilot.getState().openFeedbackModal(generatedQuery, false, userInput);
|
||||
}}
|
||||
/>
|
||||
<Separator vertical style={{ color: "#EDEBE9" }} />
|
||||
<CommandBarButton iconProps={{ iconName: "Copy" }} style={{ margin: "0 10px", backgroundColor: "#FFF8F0" }}>
|
||||
Copy code
|
||||
</CommandBarButton>
|
||||
<CommandBarButton iconProps={{ iconName: "Delete" }} style={{ backgroundColor: "#FFF8F0" }}>
|
||||
Delete code
|
||||
</CommandBarButton>
|
||||
</Stack>
|
||||
<Stack className="tabPaneContentContainer">
|
||||
<SplitterLayout vertical={true} primaryIndex={0} primaryMinSize={100} secondaryMinSize={200}>
|
||||
<EditorReact
|
||||
language={"sql"}
|
||||
content={query}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Editing Query"}
|
||||
lineNumbers={"on"}
|
||||
onContentChanged={(newQuery: string) => setQuery(newQuery)}
|
||||
onContentSelected={(selectedQuery: string) => setSelectedQuery(selectedQuery)}
|
||||
/>
|
||||
<QueryResultSection
|
||||
isMongoDB={false}
|
||||
queryEditorContent={selectedQuery || query}
|
||||
error={errorMessage}
|
||||
queryResults={queryResults}
|
||||
isExecuting={isExecuting}
|
||||
executeQueryDocumentsPage={(firstItemIndex: number) => queryDocumentsPerPage(firstItemIndex, queryIterator)}
|
||||
/>
|
||||
</SplitterLayout>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
36
src/Explorer/QueryCopilot/QueryCopilotUtilities.ts
Normal file
36
src/Explorer/QueryCopilot/QueryCopilotUtilities.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { QueryCopilotSampleContainerSchema } from "Common/Constants";
|
||||
import { handleError } from "Common/ErrorHandlingUtils";
|
||||
|
||||
interface FeedbackParams {
|
||||
likeQuery: boolean;
|
||||
generatedQuery: string;
|
||||
userPrompt: string;
|
||||
description?: string;
|
||||
contact?: string;
|
||||
}
|
||||
|
||||
export const submitFeedback = async (params: FeedbackParams): Promise<void> => {
|
||||
try {
|
||||
const { likeQuery, generatedQuery, userPrompt, description, contact } = params;
|
||||
const payload = {
|
||||
containerSchema: QueryCopilotSampleContainerSchema,
|
||||
like: likeQuery ? "like" : "dislike",
|
||||
generatedSql: generatedQuery,
|
||||
userPrompt,
|
||||
description: description || "",
|
||||
contact: contact || "",
|
||||
};
|
||||
|
||||
const response = await fetch("https://copilotorchestrater.azurewebsites.net/feedback", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
handleError(error, "copilotSubmitFeedback");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,198 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Query Copilot Carousel snapshot test should render when isOpen is true 1`] = `
|
||||
<Modal
|
||||
isOpen={true}
|
||||
styles={
|
||||
Object {
|
||||
"main": Object {
|
||||
"width": 880,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"padding": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="space-between"
|
||||
>
|
||||
<Text
|
||||
variant="xLarge"
|
||||
>
|
||||
What exactly is copilot?
|
||||
</Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Cancel",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about copilot and the background about it. The idea is to have some text to give context to the user.
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
How do you use copilot
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
To generate queries , just describe the query you want and copilot will generate the query for you.Watch this video to learn more about how to use copilot.
|
||||
</Text>
|
||||
<Image
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "16px auto",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
What is copilot good at
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about what copilot can do and its capablites with a link to
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</StyledLinkBase>
|
||||
|
||||
if possible.
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
What are its limitations
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
A couple of lines about what copilot cant do and its limitations.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
Link to documentation
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 14,
|
||||
"fontWeight": 600,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
Disclaimer
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</Stack>
|
||||
<Separator
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"height": 1,
|
||||
"padding": "16px 0",
|
||||
"selectors": Object {
|
||||
"::before": Object {
|
||||
"background": undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
horizontalAlign="start"
|
||||
verticalAlign="center"
|
||||
>
|
||||
<CustomizedPrimaryButton
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
text="Next"
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
`;
|
||||
171
src/Explorer/QueryCopilot/__snapshots__/PromptCard.test.tsx.snap
Normal file
171
src/Explorer/QueryCopilot/__snapshots__/PromptCard.test.tsx.snap
Normal file
@@ -0,0 +1,171 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Prompt card snapshot test should render properly if isSelected is false 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid #F3F2F1",
|
||||
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
"boxSizing": "border-box",
|
||||
"height": 100,
|
||||
"padding": "16px 0 16px 16px ",
|
||||
"width": 650,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem
|
||||
grow={1}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#F8FFF0",
|
||||
"color": "#00A2AD",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
Prompt
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestHeader
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 10,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestDescription
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroup
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "selected",
|
||||
"text": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey=""
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Object {
|
||||
"width": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
|
||||
exports[`Prompt card snapshot test should render properly if isSelected is true 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"border": "1px solid #F3F2F1",
|
||||
"boxShadow": "0px 1.6px 3.6px rgba(0, 0, 0, 0.132), 0px 0.3px 0.9px rgba(0, 0, 0, 0.108)",
|
||||
"boxSizing": "border-box",
|
||||
"height": 100,
|
||||
"padding": "16px 0 16px 16px ",
|
||||
"width": 650,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem
|
||||
grow={1}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"background": "#F8FFF0",
|
||||
"color": "#00A2AD",
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
>
|
||||
Prompt
|
||||
</Text>
|
||||
</div>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 13,
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestHeader
|
||||
</Text>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 10,
|
||||
"marginTop": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
TestDescription
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 16,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledChoiceGroup
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "selected",
|
||||
"text": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
selectedKey="selected"
|
||||
styles={
|
||||
Object {
|
||||
"flexContainer": Object {
|
||||
"width": 36,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
`;
|
||||
@@ -0,0 +1,211 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Query copilot tab snapshot test should render with initial input 1`] = `
|
||||
<Stack
|
||||
className="tab-pane"
|
||||
style={
|
||||
Object {
|
||||
"height": "100%",
|
||||
"padding": 24,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Image
|
||||
src=""
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 16,
|
||||
"fontWeight": 600,
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
Copilot
|
||||
</Text>
|
||||
</Stack>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 16,
|
||||
"width": "100%",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"lineHeight": 30,
|
||||
}
|
||||
}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"width": "90%",
|
||||
},
|
||||
}
|
||||
}
|
||||
value="Write a query to return all records in this table"
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
disabled={false}
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Send",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 8,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"marginBottom": 24,
|
||||
"marginTop": 8,
|
||||
}
|
||||
}
|
||||
>
|
||||
AI-generated content can have mistakes. Make sure it's accurate and appropriate before using it.
|
||||
|
||||
<StyledLinkBase
|
||||
href=""
|
||||
target="_blank"
|
||||
>
|
||||
Read preview terms
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Stack
|
||||
horizontal={true}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#FFF8F0",
|
||||
"padding": "2px 8px",
|
||||
}
|
||||
}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"fontSize": 12,
|
||||
"fontWeight": 600,
|
||||
}
|
||||
}
|
||||
>
|
||||
Provide feedback on the query generated
|
||||
</Text>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Like",
|
||||
}
|
||||
}
|
||||
id="likeBtn"
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"marginLeft": 20,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<CustomizedIconButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Dislike",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"margin": "0 10px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Separator
|
||||
style={
|
||||
Object {
|
||||
"color": "#EDEBE9",
|
||||
}
|
||||
}
|
||||
vertical={true}
|
||||
/>
|
||||
<CustomizedCommandBarButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Copy",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#FFF8F0",
|
||||
"margin": "0 10px",
|
||||
}
|
||||
}
|
||||
>
|
||||
Copy code
|
||||
</CustomizedCommandBarButton>
|
||||
<CustomizedCommandBarButton
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Delete",
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#FFF8F0",
|
||||
}
|
||||
}
|
||||
>
|
||||
Delete code
|
||||
</CustomizedCommandBarButton>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="tabPaneContentContainer"
|
||||
>
|
||||
<t
|
||||
customClassName=""
|
||||
onDragEnd={null}
|
||||
onDragStart={null}
|
||||
onSecondaryPaneSizeChange={null}
|
||||
percentage={false}
|
||||
primaryIndex={0}
|
||||
primaryMinSize={100}
|
||||
secondaryMinSize={200}
|
||||
vertical={true}
|
||||
>
|
||||
<EditorReact
|
||||
ariaLabel="Editing Query"
|
||||
content=""
|
||||
isReadOnly={false}
|
||||
language="sql"
|
||||
lineNumbers="on"
|
||||
onContentChanged={[Function]}
|
||||
onContentSelected={[Function]}
|
||||
/>
|
||||
<QueryResultSection
|
||||
error=""
|
||||
executeQueryDocumentsPage={[Function]}
|
||||
isExecuting={false}
|
||||
isMongoDB={false}
|
||||
queryEditorContent=""
|
||||
/>
|
||||
</t>
|
||||
</Stack>
|
||||
</Stack>
|
||||
`;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user