mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 10:51:30 +00:00
Compare commits
55 Commits
postgres_q
...
accessibil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb04af4c6 | ||
|
|
7f220bf8be | ||
|
|
1ee6abf890 | ||
|
|
a2b0cf236e | ||
|
|
e2b72a3824 | ||
|
|
f27706bc37 | ||
|
|
72c3605dbe | ||
|
|
fa81442222 | ||
|
|
25ef86a6c0 | ||
|
|
a4e60f106c | ||
|
|
d646686723 | ||
|
|
b84d5b572c | ||
|
|
a7a38807df | ||
|
|
c711b59f7d | ||
|
|
e945963cf9 | ||
|
|
5a660551c6 | ||
|
|
1285ffc440 | ||
|
|
b4bca3d41a | ||
|
|
2a47e4311c | ||
|
|
49d9f489d8 | ||
|
|
31cc129aa7 | ||
|
|
99af4acca4 | ||
|
|
80dd161a6f | ||
|
|
b2d59f3b3f | ||
|
|
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
|
||||
|
||||
@@ -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,8 +10,9 @@
|
||||
<link rel="stylesheet" href="../assets/css/main.css">
|
||||
<script async src="../assets/js/search.js" id="search-script"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<header>
|
||||
<div class="tsd-page-toolbar">
|
||||
<div class="container">
|
||||
<div class="table-wrap">
|
||||
@@ -64,8 +66,8 @@
|
||||
<h1>Enumeration BladeType</h1>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="container container-main">
|
||||
</header>
|
||||
<div class="container container-main">
|
||||
<div class="row">
|
||||
<div class="col-8 col-content">
|
||||
<section class="tsd-panel tsd-comment">
|
||||
@@ -82,12 +84,24 @@
|
||||
<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>
|
||||
<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>
|
||||
@@ -98,31 +112,36 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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">
|
||||
@@ -134,36 +153,41 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<p>Keys blade of a Azure Cosmos DB for Table account.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -179,19 +203,23 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<a
|
||||
href="../modules/selfserve_selfserveutils.html">Self<wbr>Serve/<wbr>Self<wbr>Serve<wbr>Utils</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@@ -203,39 +231,47 @@
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<a href="../modules/selfserve_selfserveutils.html#generatebladelink"
|
||||
class="tsd-kind-icon">generate<wbr>Blade<wbr>Link</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="with-border-bottom">
|
||||
</div>
|
||||
<footer class="with-border-bottom">
|
||||
<div class="container">
|
||||
<h2>Legend</h2>
|
||||
<div class="tsd-legend-group">
|
||||
@@ -254,11 +290,12 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<div class="container tsd-generator">
|
||||
</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>
|
||||
<div class="overlay"></div>
|
||||
<script src="../assets/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
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 |
@@ -6,7 +6,7 @@
|
||||
display: table;
|
||||
display: none;
|
||||
width: 100%;
|
||||
border-top: 1px solid #DDDDDD;
|
||||
border-top: 1px solid #dddddd;
|
||||
/*[{environment-commandbar-toolbar-separator}]*/
|
||||
background-color: #ffffff;
|
||||
/*[{plugin-background-color}]*/
|
||||
@@ -34,7 +34,7 @@
|
||||
}
|
||||
|
||||
.query-builder {
|
||||
width:100%;
|
||||
width: 100%;
|
||||
padding-right: @DefaultSpace;
|
||||
border-bottom: 1px solid @BaseMedium;
|
||||
margin-bottom: @DefaultSpace;
|
||||
@@ -45,7 +45,7 @@
|
||||
/*[{plugin-background-color}]*/
|
||||
min-width: 600px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid #DDDDDD;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
/*[1px solid {environment-commandbar-toolbar-separator}]*/
|
||||
}
|
||||
|
||||
@@ -70,14 +70,14 @@
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:hover {
|
||||
background-color: #CCCEDB;
|
||||
background-color: #cccedb;
|
||||
/*[{common-controls-button-hover-background}]*/
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button.active {
|
||||
background-color: #E6E7ED;
|
||||
background-color: #e6e7ed;
|
||||
/*[{common-controls-inner-tab-active-background}]*/
|
||||
outline: none
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.query-builder-toolbar .query-toolbar-group .query-toolbar-button:disabled,
|
||||
@@ -96,10 +96,10 @@
|
||||
.flex-direction();
|
||||
}
|
||||
|
||||
.tablesQueryTab{
|
||||
.tablesQueryTab {
|
||||
padding-left: @MediumSpace;
|
||||
width: 100%;
|
||||
margin-bottom:@LargeSpace;
|
||||
margin-bottom: @LargeSpace;
|
||||
}
|
||||
|
||||
.entity-error-Img {
|
||||
@@ -120,7 +120,7 @@
|
||||
.query-editor-text {
|
||||
width: 100%;
|
||||
margin: 2px;
|
||||
border: solid 1px #A9ACB3;
|
||||
border: solid 1px #a9acb3;
|
||||
/*[{plugin-textbox-disabled-color}]*/
|
||||
resize: none;
|
||||
margin-top: -39px;
|
||||
@@ -169,7 +169,7 @@
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.advanced-options-panel{
|
||||
.advanced-options-panel {
|
||||
margin-bottom: @DefaultSpace;
|
||||
}
|
||||
|
||||
@@ -201,9 +201,9 @@ input::-webkit-inner-spin-button {
|
||||
.advanced-options-panel .advanced-options .top .top-input {
|
||||
width: 100px;
|
||||
word-spacing: normal;
|
||||
color: #1E1E1E;
|
||||
color: #1e1e1e;
|
||||
/*[{common-controls-button-foreground}]*/
|
||||
border: 1px solid #CCCEDB;
|
||||
border: 1px solid #cccedb;
|
||||
/*[1px solid {plugin-textbox-border-color}]*/
|
||||
height: 20px;
|
||||
margin-left: 8px;
|
||||
@@ -257,9 +257,9 @@ input::-webkit-inner-spin-button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.action-column>button,
|
||||
.group-control-header>button,
|
||||
.group-indicator-column>button {
|
||||
.action-column > button,
|
||||
.group-control-header > button,
|
||||
.group-indicator-column > button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
@@ -268,7 +268,7 @@ input::-webkit-inner-spin-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-control-header>button:disabled {
|
||||
.group-control-header > button:disabled {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
padding: 0px;
|
||||
@@ -299,9 +299,9 @@ input::-webkit-inner-spin-button {
|
||||
}
|
||||
|
||||
.scroll-box {
|
||||
border-bottom: 1px transparent #DDD;
|
||||
border-bottom: 1px transparent #ddd;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
border-top: 1px transparent #DDD;
|
||||
border-top: 1px transparent #ddd;
|
||||
/*[1px solid {plugin-table-border-color}]*/
|
||||
max-height: 20vh;
|
||||
width: 100%;
|
||||
@@ -366,7 +366,7 @@ input::-webkit-inner-spin-button {
|
||||
|
||||
.group-indicator-table {
|
||||
border-spacing: 0px;
|
||||
min-height: 24px
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.group-indicator-column {
|
||||
@@ -396,7 +396,6 @@ input::-webkit-inner-spin-button {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
/*.type-header {
|
||||
padding-right: 4px;
|
||||
}
|
||||
@@ -410,9 +409,9 @@ input::-webkit-inner-spin-button {
|
||||
}*/
|
||||
|
||||
.clause-table-field[readonly] {
|
||||
background-color: #EEEEF2;
|
||||
background-color: #eeeef2;
|
||||
/*[{plugin-table-header-background-color}]*/
|
||||
border: 1px solid #CCCEDB;
|
||||
border: 1px solid #cccedb;
|
||||
/*[{plugin-table-border-color}]*/
|
||||
}
|
||||
|
||||
@@ -462,11 +461,11 @@ input::-webkit-inner-spin-button {
|
||||
|
||||
.query-panel .divider.horizontal {
|
||||
height: 10px;
|
||||
width: 100%
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inline-div {
|
||||
display: inline
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.querybuilder-addpropertyImg,
|
||||
@@ -485,7 +484,7 @@ input::-webkit-inner-spin-button {
|
||||
}
|
||||
|
||||
.entity-Add-Cancel {
|
||||
padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||
// padding: @DefaultSpace @SmallSpace @SmallSpace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -498,7 +497,7 @@ input::-webkit-inner-spin-button {
|
||||
}
|
||||
|
||||
.query-builder-isDisabled {
|
||||
border: 1px solid #CCCEDB;
|
||||
border: 1px solid #cccedb;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
@@ -515,7 +514,6 @@ input::-webkit-inner-spin-button {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@media only screen and (max-width: 1200px) {
|
||||
.clause-table {
|
||||
|
||||
@@ -2576,6 +2576,10 @@ a:link {
|
||||
.querydropdown.placeholderVisible {
|
||||
font-style: italic;
|
||||
}
|
||||
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||
color: #403f3f;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.querydropdown:hover {
|
||||
background-color: @AccentLow;
|
||||
@@ -3087,3 +3091,4 @@ a:link {
|
||||
background: white;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
@@ -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;
|
||||
|
||||
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"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
@@ -566,6 +568,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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -310,7 +310,9 @@ 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>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ interface ThroughputInputAutoPilotV3State {
|
||||
spendAckChecked: boolean;
|
||||
exceedFreeTierThroughput: boolean;
|
||||
}
|
||||
|
||||
export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
ThroughputInputAutoPilotV3Props,
|
||||
ThroughputInputAutoPilotV3State
|
||||
@@ -624,7 +623,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>
|
||||
)}
|
||||
|
||||
@@ -15,6 +15,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
"iconName": "WarningSolid",
|
||||
}
|
||||
}
|
||||
role="alert"
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('[id="Manual-input"]').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('[id="Autoscale-input"]').simulate("change");
|
||||
expect(wrapper.find('[aria-label="Throughput header"]').at(0).text()).toBe("Container throughput (autoscale)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -185,9 +185,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
id="Autoscale-input"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-label={`Autoscale` + " " + getThroughputLabelText()}
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
@@ -195,11 +197,14 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Autoscale")}
|
||||
/>
|
||||
<span className="throughputInputRadioBtnLabel">Autoscale</span>
|
||||
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
|
||||
Autoscale
|
||||
</label>
|
||||
|
||||
<input
|
||||
id="Manual-input"
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Manual mode"
|
||||
aria-label={`Manual` + " " + getThroughputLabelText()}
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
@@ -207,16 +212,25 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
/>
|
||||
<span className="throughputInputRadioBtnLabel">Manual</span>
|
||||
<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">
|
||||
<u style={{ color: "#0078D4" }}>
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://cosmos.azure.com/capacitycalculator/"
|
||||
aria-label="capacity calculator of azure cosmos db"
|
||||
>
|
||||
capacity calculator
|
||||
</Link>
|
||||
</u>
|
||||
.
|
||||
</Text>
|
||||
|
||||
@@ -237,8 +251,9 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
onChange={(event, newInput?: string) => onThroughputValueChange(newInput)}
|
||||
step={AutoPilotUtils.autoPilotIncrementStep}
|
||||
min={AutoPilotUtils.autoPilotThroughput1K}
|
||||
max={!isSharded ? 10000 : 9000000000000}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Max request units per second"
|
||||
required={true}
|
||||
errorMessage={throughputError}
|
||||
/>
|
||||
@@ -258,9 +273,11 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<Stack className="throughputInputSpacing">
|
||||
<Text variant="small" aria-label="ruDescription">
|
||||
Estimate your required RU/s with
|
||||
<u style={{ color: "#0078D4" }}>
|
||||
<Link target="_blank" href="https://cosmos.azure.com/capacitycalculator/" aria-label="capacityLink">
|
||||
capacity calculator
|
||||
</Link>
|
||||
</u>
|
||||
.
|
||||
</Text>
|
||||
|
||||
@@ -283,7 +300,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
min={SharedConstants.CollectionCreation.DefaultCollectionRUs400}
|
||||
max={userContext.isTryCosmosDBSubscription ? Constants.TryCosmosExperience.maxRU : Infinity}
|
||||
value={throughput.toString()}
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Max request units per second"
|
||||
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"
|
||||
@@ -653,41 +653,46 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<div
|
||||
className="ms-Stack css-58"
|
||||
>
|
||||
<div
|
||||
key=".0:$.0"
|
||||
role="radiogroup"
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-label="Autoscale Container throughput (autoscale)"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.0"
|
||||
id="Autoscale-input"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
<label
|
||||
className="throughputInputRadioBtnLabel"
|
||||
key=".0:$.1"
|
||||
htmlFor="Autoscale-input"
|
||||
>
|
||||
Autoscale
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-label="Manual Container throughput (autoscale)"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
id="Manual-input"
|
||||
onChange={[Function]}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
type="radio"
|
||||
/>
|
||||
<span
|
||||
<label
|
||||
className="throughputInputRadioBtnLabel"
|
||||
key=".0:$.3"
|
||||
htmlFor="Manual-input"
|
||||
>
|
||||
Manual
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack
|
||||
@@ -697,23 +702,30 @@ 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
|
||||
|
||||
<u
|
||||
style={
|
||||
Object {
|
||||
"color": "#0078D4",
|
||||
}
|
||||
}
|
||||
>
|
||||
<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 +1004,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]}
|
||||
@@ -1002,6 +1014,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</a>
|
||||
</LinkBase>
|
||||
</StyledLinkBase>
|
||||
</u>
|
||||
.
|
||||
</span>
|
||||
</Text>
|
||||
@@ -1331,13 +1344,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 +1630,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,10 +1648,11 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</div>
|
||||
</Stack>
|
||||
<StyledTextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Max request units per second"
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
key=".0:$.2"
|
||||
max={9000000000000}
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
@@ -1658,10 +1672,11 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
value="4000"
|
||||
>
|
||||
<TextFieldBase
|
||||
aria-label="Max request units per second"
|
||||
ariaLabel="Max request units per second"
|
||||
deferredValidationTime={200}
|
||||
errorMessage=""
|
||||
id="autoscaleRUValueField"
|
||||
max={9000000000000}
|
||||
min={1000}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
@@ -1956,8 +1971,10 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Max request units per second"
|
||||
className="ms-TextField-field field-64"
|
||||
id="autoscaleRUValueField"
|
||||
max={9000000000000}
|
||||
min={1000}
|
||||
onBlur={[Function]}
|
||||
onChange={[Function]}
|
||||
|
||||
@@ -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,11 +3,11 @@
|
||||
.treeComponent {
|
||||
.nodeItem {
|
||||
&:focus {
|
||||
outline: 1px dashed @AccentMedium;
|
||||
outline: 2px @AccentMedium;
|
||||
}
|
||||
|
||||
.treeNodeHeader {
|
||||
padding: @SmallSpace 2px;
|
||||
margin: 1px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
|
||||
@@ -133,7 +133,6 @@ describe("ContainerSampleGenerator", () => {
|
||||
} as DatabaseAccount,
|
||||
});
|
||||
|
||||
// Rejects with error that contains experience
|
||||
expect(ContainerSampleGenerator.createSampleGeneratorAsync(explorerStub)).rejects.toMatch(experience);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,10 +186,8 @@ export default class Explorer {
|
||||
useNotebook.getState().setNotebookBasePath(userContext.features.notebookBasePath);
|
||||
}
|
||||
|
||||
if (!userContext.features.enablePGQuickstart || userContext.apiType !== "Postgres") {
|
||||
this.refreshExplorer();
|
||||
}
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
if (!this.notebookManager) {
|
||||
@@ -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 = {
|
||||
@@ -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> {
|
||||
if (userContext.apiType !== "Postgres") {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases();
|
||||
}
|
||||
await useNotebook.getState().refreshNotebooksEnabledStateForAccount();
|
||||
|
||||
// TODO: remove reference to isNotebookEnabled and isNotebooksEnabledForAccount
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -17,19 +17,19 @@ 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="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} />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const buttons = useCommandBar((state) => state.contextButtons);
|
||||
const backgroundColor = StyleConstants.BaseLight;
|
||||
|
||||
if (userContext.features.enablePGQuickstart && userContext.apiType === "Postgres") {
|
||||
if (userContext.apiType === "Postgres") {
|
||||
const buttons = CommandBarComponentButtonFactory.createPostgreButtons(container);
|
||||
return (
|
||||
<div className="commandBarContainer">
|
||||
|
||||
@@ -202,6 +202,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 +524,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 +610,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 (
|
||||
<>
|
||||
<div style={{ padding: "34px" }}>
|
||||
<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.
|
||||
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>
|
||||
<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" }}
|
||||
/>
|
||||
</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");
|
||||
window.open("https://cosmos.azure.com/", "_blank");
|
||||
}}
|
||||
text="Open"
|
||||
iconProps={{ iconName: "OpenInNewWindow" }}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,11 +262,19 @@ 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">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.createNewDatabase}
|
||||
@@ -290,6 +301,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
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: 250, 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 (preview)
|
||||
</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 preview
|
||||
version of .NET V3 or Java V4 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,11 +794,18 @@ 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">
|
||||
<div role="radiogroup">
|
||||
<input
|
||||
className="panelRadioBtn"
|
||||
checked={this.state.enableAnalyticalStore}
|
||||
@@ -703,6 +835,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
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" && (
|
||||
<Stack className="panelGroupSpacing">
|
||||
<Checkbox
|
||||
label="My partition key is larger than 101 bytes"
|
||||
checked={this.state.useHashV2}
|
||||
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" },
|
||||
label: { padding: 0, alignItems: "center", wordWrap: "break-word", whiteSpace: "break-spaces" },
|
||||
}}
|
||||
onChange={(ev: React.FormEvent<HTMLElement>, isChecked: boolean) =>
|
||||
this.setState({ useHashV2: isChecked })
|
||||
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="link" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
|
||||
More details
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
@@ -40,6 +41,9 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<div
|
||||
role="radiogroup"
|
||||
>
|
||||
<input
|
||||
aria-checked={true}
|
||||
@@ -74,6 +78,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
>
|
||||
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": 250,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
Add hierarchical partition key (preview)
|
||||
</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}
|
||||
@@ -312,6 +346,9 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<div
|
||||
role="radiogroup"
|
||||
>
|
||||
<input
|
||||
aria-checked={false}
|
||||
@@ -349,6 +386,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
>
|
||||
Off
|
||||
</span>
|
||||
</div>
|
||||
</Stack>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
@@ -395,10 +433,13 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
id="collapsibleSectionContent"
|
||||
>
|
||||
<Stack
|
||||
className="panelGroupSpacing"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
checked={false}
|
||||
label="My partition key is larger than 101 bytes"
|
||||
label="My application uses an older Cosmos .NET or Java SDK version (.NET V1 or Java V2)"
|
||||
onChange={[Function]}
|
||||
styles={
|
||||
Object {
|
||||
@@ -409,6 +450,8 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
"label": Object {
|
||||
"alignItems": "center",
|
||||
"padding": 0,
|
||||
"whiteSpace": "break-spaces",
|
||||
"wordWrap": "break-word",
|
||||
},
|
||||
"text": Object {
|
||||
"fontSize": 12,
|
||||
@@ -416,6 +459,24 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
|
||||
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal file
105
src/Explorer/Quickstart/PostgreQuickstartCommands.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
export const newTableCommand = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
|
||||
CREATE SCHEMA cosmosdb_tutorial;
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
avatar_url text,
|
||||
gravatar_id text,
|
||||
display_login text
|
||||
);
|
||||
CREATE TABLE github_events
|
||||
(
|
||||
event_id bigint,
|
||||
event_type text,
|
||||
event_public boolean,
|
||||
repo_id bigint,
|
||||
payload jsonb,
|
||||
repo jsonb,
|
||||
user_id bigint,
|
||||
org jsonb,
|
||||
created_at timestamp
|
||||
);
|
||||
CREATE INDEX event_type_index ON github_events (event_type);
|
||||
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);
|
||||
`;
|
||||
|
||||
export const newTableCommandForDisplay = `DROP SCHEMA IF EXISTS cosmosdb_tutorial CASCADE;
|
||||
CREATE SCHEMA cosmosdb_tutorial;
|
||||
|
||||
-- Using schema created for tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
avatar_url text,
|
||||
gravatar_id text,
|
||||
display_login text
|
||||
);
|
||||
|
||||
CREATE TABLE github_events
|
||||
(
|
||||
event_id bigint,
|
||||
event_type text,
|
||||
event_public boolean,
|
||||
repo_id bigint,
|
||||
payload jsonb,
|
||||
repo jsonb,
|
||||
user_id bigint,
|
||||
org jsonb,
|
||||
created_at timestamp
|
||||
);
|
||||
|
||||
--Create indexes on events table
|
||||
CREATE INDEX event_type_index ON github_events (event_type);
|
||||
CREATE INDEX payload_index ON github_events USING GIN (payload jsonb_path_ops);`;
|
||||
|
||||
export const distributeTableCommand = `SET search_path to cosmosdb_tutorial;
|
||||
SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id');
|
||||
`;
|
||||
|
||||
export const distributeTableCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id');`;
|
||||
|
||||
export const loadDataCommand = `SET search_path to cosmosdb_tutorial;
|
||||
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
|
||||
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);
|
||||
`;
|
||||
|
||||
export const loadDataCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- download users and store in table
|
||||
\\COPY github_users FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/users.csv"' WITH (FORMAT CSV);
|
||||
\\COPY github_events FROM PROGRAM 'wget -q -O - "$@" "https://examples.citusdata.com/events.csv"' WITH (FORMAT CSV);`;
|
||||
|
||||
export const queryCommand = `SET search_path to cosmosdb_tutorial;
|
||||
SELECT count(*) FROM github_users;
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;
|
||||
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;
|
||||
`;
|
||||
|
||||
export const queryCommandForDisplay = `-- Using schema created for the tutorial
|
||||
SET search_path to cosmosdb_tutorial;
|
||||
|
||||
-- count all rows (across shards)
|
||||
SELECT count(*) FROM github_users;
|
||||
|
||||
-- Find all events for a single user.
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;
|
||||
|
||||
-- Find the number of commits on the master branch per hour
|
||||
SELECT date_trunc('hour', created_at) AS hour, sum((payload->>'distinct_size')::int) AS num_commits FROM github_events WHERE event_type = 'PushEvent' AND payload @> '{"ref":"refs/heads/master"}' GROUP BY hour ORDER BY hour;`;
|
||||
@@ -94,7 +94,7 @@ const getDescriptionText = (page: number): string => {
|
||||
case 1:
|
||||
return "Azure Cosmos DB is a fully managed NoSQL database service for modern app development. ";
|
||||
case 2:
|
||||
return "Launch the quickstart for a tutotrial to learn how to create a database, add sample data, connect to a sample app and more.";
|
||||
return "Launch the quickstart for a tutorial to learn how to create a database, add sample data, connect to a sample app and more.";
|
||||
case 3:
|
||||
return "Already have an existing app? Connect your database to an app, or tooling of your choice from Data Explorer.";
|
||||
default:
|
||||
|
||||
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal file
22
src/Explorer/Quickstart/QuickstartFirewallNotification.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Image, PrimaryButton, Stack, Text } from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import React from "react";
|
||||
import FirewallRuleScreenshot from "../../../images/firewallRule.png";
|
||||
|
||||
export const QuickstartFirewallNotification: React.FC = (): JSX.Element => (
|
||||
<Stack style={{ padding: "16px 20px" }}>
|
||||
<Text block>
|
||||
To use the PostgreSQL shell, you need to add a firewall rule to allow access from all IP addresses
|
||||
(0.0.0.0-255.255.255).
|
||||
</Text>
|
||||
<Text block>We strongly recommend removing this rule once you finish using the PostgreSQL shell.</Text>
|
||||
<Image style={{ margin: "20px 0" }} src={FirewallRuleScreenshot} />
|
||||
<PrimaryButton
|
||||
style={{ width: 150 }}
|
||||
onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}
|
||||
>
|
||||
Add firewall rule
|
||||
</PrimaryButton>
|
||||
</Stack>
|
||||
);
|
||||
@@ -11,6 +11,17 @@ import {
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import {
|
||||
distributeTableCommand,
|
||||
distributeTableCommandForDisplay,
|
||||
loadDataCommand,
|
||||
loadDataCommandForDisplay,
|
||||
newTableCommand,
|
||||
newTableCommandForDisplay,
|
||||
queryCommand,
|
||||
queryCommandForDisplay,
|
||||
} from "Explorer/Quickstart/PostgreQuickstartCommands";
|
||||
import { useTerminal } from "hooks/useTerminal";
|
||||
import React, { useState } from "react";
|
||||
import Youtube from "react-youtube";
|
||||
import Pivot1SelectedIcon from "../../../images/Pivot1_selected.svg";
|
||||
@@ -35,24 +46,6 @@ enum GuideSteps {
|
||||
|
||||
export const QuickstartGuide: React.FC = (): JSX.Element => {
|
||||
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||
const newTableCommand = `CREATE TABLE github_users
|
||||
(
|
||||
user_id bigint,
|
||||
url text,
|
||||
login text,
|
||||
.....`;
|
||||
const distributeTableCommand = `SELECT create_distributed_table('github_users', 'user_id');
|
||||
SELECT create_distributed_table('github_events', 'user_id');`;
|
||||
const loadDataCommand = `-- download users and store in table
|
||||
|
||||
COPY github_users FROM PROGRAM 'curl https://examples.citusdata.com/
|
||||
users.csv' WITH (FORMAT CSV)`;
|
||||
const queryCommand = `-- Find all events for a single user.
|
||||
-- (A common transactional/operational query)
|
||||
|
||||
SELECT created_at, event_type, repo->>'name' AS repo_name
|
||||
FROM github_events
|
||||
WHERE user_id = 3861633;`;
|
||||
|
||||
const onCopyBtnClicked = (selector: string): void => {
|
||||
const textfield: HTMLInputElement = document.querySelector(selector);
|
||||
@@ -102,24 +95,32 @@ WHERE user_id = 3861633;`;
|
||||
<Stack style={{ paddingTop: 8, height: "100%", width: "100%" }}>
|
||||
<Stack style={{ flexGrow: 1, padding: "0 20px", overflow: "auto" }}>
|
||||
<Text variant="xxLarge">Quick start guide</Text>
|
||||
<Text variant="medium">Gettings started in Cosmos DB</Text>
|
||||
{currentStep < 5 && (
|
||||
<Pivot style={{ marginTop: 10, width: "100%" }} selectedKey={GuideSteps[currentStep]}>
|
||||
<Pivot
|
||||
style={{ marginTop: 10, width: "100%" }}
|
||||
selectedKey={GuideSteps[currentStep]}
|
||||
onLinkClick={(item?: PivotItem) => setCurrentStep(Object.values(GuideSteps).indexOf(item.props.itemKey))}
|
||||
>
|
||||
<PivotItem
|
||||
headerText="Login"
|
||||
onRenderItemLink={(props, defaultRenderer) => customPivotHeaderRenderer(props, defaultRenderer, 0)}
|
||||
itemKey={GuideSteps[0]}
|
||||
onClick={() => setCurrentStep(0)}
|
||||
onClick={() => {
|
||||
setCurrentStep(0);
|
||||
}}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
This tutorial walks you through the most essential Cosmos DB PostgreSQL statements that will be used
|
||||
in the PostgreSQL shell (on the right). You can also choose to go through this quick start by
|
||||
connecting to PGAdmin or other tooling of your choice. <br />
|
||||
<br /> Before you can interact with your data using PGShell, you will need to login - please follow
|
||||
instructions on the right to enter your password
|
||||
This tutorial guides you to create and query distributed tables using a sample dataset.
|
||||
<br />
|
||||
<br />
|
||||
To begin, please enter the cluster's password in the PostgreSQL terminal.
|
||||
<br />
|
||||
<br />
|
||||
Note: If you navigate out of the Quick Start tab (PostgreSQL Shell), the session will be closed and
|
||||
all ongoing commands might be interrupted.
|
||||
</Text>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "60%" }} />
|
||||
<Youtube videoId="nT64dFSfiUo" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -129,19 +130,28 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => setCurrentStep(1)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
After logging in, let’s create some new tables for storing data. We will start with two sample tables
|
||||
- one for storing github users and one for storing github events
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 110 }}>New table</DefaultButton>
|
||||
<Text>Let's create two tables github_users and github_events in “cosmosdb_tutorial” schema.</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 150 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(newTableCommand)}
|
||||
>
|
||||
Create new table
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="newTableCommand"
|
||||
multiline
|
||||
rows={6}
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={newTableCommand}
|
||||
styles={{ root: { width: "90%" } }}
|
||||
defaultValue={newTableCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
@@ -150,7 +160,7 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => onCopyBtnClicked("#newTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "60%" }} />
|
||||
<Youtube videoId="il_sA6U1WcY" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -161,21 +171,32 @@ WHERE user_id = 3861633;`;
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
Congratulations, you have now created your first 2 tables.
|
||||
Let’s distribute the two tables using the create_distributed_table() function.
|
||||
<br />
|
||||
<br />
|
||||
Your table needs to be sharded on the worker nodes. You need to distribute table before you load any
|
||||
data or run any queries
|
||||
We are choosing “user_id” as the distribution column for our sample dataset.
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 150 }}>Distribute table</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 200 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(distributeTableCommand)}
|
||||
>
|
||||
Create distributed table
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="distributeTableCommand"
|
||||
multiline
|
||||
rows={2}
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={distributeTableCommand}
|
||||
styles={{ root: { width: "90%" } }}
|
||||
defaultValue={distributeTableCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
@@ -184,7 +205,7 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => onCopyBtnClicked("#distributeTableCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "60%" }} />
|
||||
<Youtube videoId="kCCDRRrN1r0" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -194,22 +215,28 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => setCurrentStep(3)}
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
We're ready to fill the tables with sample data.
|
||||
<br />
|
||||
<br />
|
||||
For this quick start, we'll use a dataset previously captured from the GitHub API. Run the
|
||||
command below to load the data
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 110 }}>Load data</DefaultButton>
|
||||
<Text>Let's load the two tables with a sample dataset generated from the GitHub API.</Text>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 110 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(loadDataCommand)}
|
||||
>
|
||||
Load data
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="loadDataCommand"
|
||||
multiline
|
||||
rows={4}
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={loadDataCommand}
|
||||
styles={{ root: { width: "90%" } }}
|
||||
defaultValue={loadDataCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
@@ -218,7 +245,7 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => onCopyBtnClicked("#loadDataCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "60%" }} />
|
||||
<Youtube videoId="XSMEE2tujEk" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
<PivotItem
|
||||
@@ -229,19 +256,29 @@ WHERE user_id = 3861633;`;
|
||||
>
|
||||
<Stack style={{ marginTop: 20 }}>
|
||||
<Text>
|
||||
github_users is a distributed table, meaning its data is divided between multiple shards. Hyperscale
|
||||
(Citus) automatically runs the count on all shards in parallel, and combines the results. Let’s try a
|
||||
query.
|
||||
Congratulations on creating and distributing your tables. Now, it's time to run your first query!
|
||||
</Text>
|
||||
<DefaultButton style={{ marginTop: 16, width: 110 }}>Try query</DefaultButton>
|
||||
<DefaultButton
|
||||
style={{ marginTop: 16, width: 115 }}
|
||||
onClick={() => useTerminal.getState().sendMessage(queryCommand)}
|
||||
>
|
||||
Try queries
|
||||
</DefaultButton>
|
||||
<Stack horizontal style={{ marginTop: 16 }}>
|
||||
<TextField
|
||||
id="queryCommand"
|
||||
multiline
|
||||
rows={6}
|
||||
rows={5}
|
||||
readOnly
|
||||
defaultValue={queryCommand}
|
||||
styles={{ root: { width: "90%" } }}
|
||||
defaultValue={queryCommandForDisplay}
|
||||
styles={{
|
||||
root: { width: "90%" },
|
||||
field: {
|
||||
backgroundColor: "#EEEEEE",
|
||||
fontFamily:
|
||||
"Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
iconProps={{
|
||||
@@ -250,7 +287,7 @@ WHERE user_id = 3861633;`;
|
||||
onClick={() => onCopyBtnClicked("#queryCommand")}
|
||||
/>
|
||||
</Stack>
|
||||
<Youtube videoId="Jvgh64rvdXU" style={{ margin: "20px 0" }} opts={{ width: "60%" }} />
|
||||
<Youtube videoId="k_EanjMtaPg" style={{ margin: "20px 0" }} opts={{ width: "90%" }} />
|
||||
</Stack>
|
||||
</PivotItem>
|
||||
</Pivot>
|
||||
@@ -258,7 +295,7 @@ WHERE user_id = 3861633;`;
|
||||
{currentStep === 5 && (
|
||||
<Stack style={{ margin: "auto" }} horizontalAlign="center">
|
||||
<Image src={CompleteIcon} />
|
||||
<Text variant="mediumPlus" style={{ fontWeight: 600, marginTop: 7 }}>
|
||||
<Text variant="mediumPlus" style={{ fontWeight: 900, marginTop: 7 }}>
|
||||
You are all set!
|
||||
</Text>
|
||||
<Text variant="mediumPlus" style={{ marginTop: 8 }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { DirectionalHint, Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
@@ -18,6 +18,11 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
let totalSteps = 9;
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
totalSteps = 10;
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
return isSampleDBExpanded ? (
|
||||
@@ -33,7 +38,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
},
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 1 of 8"
|
||||
footerContent={"Step 1 of " + totalSteps}
|
||||
>
|
||||
Start viewing and working with your data by opening Documents under Data
|
||||
</TeachingBubble>
|
||||
@@ -55,7 +60,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(1),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 2 of 8"
|
||||
footerContent={"Step 2 of " + totalSteps}
|
||||
>
|
||||
View documents here using the documents window. You can also use your favorite MongoDB tools and drivers to do
|
||||
so.
|
||||
@@ -78,7 +83,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(2),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 3 of 8"
|
||||
footerContent={"Step 3 of " + totalSteps}
|
||||
>
|
||||
Add new document by copy / pasting JSON or uploading a JSON. You can also use your favorite MongoDB tools and
|
||||
drivers to do so.
|
||||
@@ -99,9 +104,9 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(3),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 4 of 8"
|
||||
footerContent={"Step 4 of " + totalSteps}
|
||||
>
|
||||
Query your data using the filter function. Azure Cosmos DB API for MongoDB provides comprehensive support for
|
||||
Query your data using the filter function. Azure Cosmos DB for MongoDB provides comprehensive support for
|
||||
MongoDB query language constructs. You can also use your favorite MongoDB tools and drivers to do so.
|
||||
</TeachingBubble>
|
||||
);
|
||||
@@ -120,7 +125,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(4),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 5 of 8"
|
||||
footerContent={"Step 5 of " + totalSteps}
|
||||
>
|
||||
Change throughput provisioned to your collection according to your needs
|
||||
</TeachingBubble>
|
||||
@@ -140,7 +145,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(5),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 6 of 8"
|
||||
footerContent={"Step 6 of " + totalSteps}
|
||||
>
|
||||
Use the indexing policy editor to create and edit your indexes.
|
||||
</TeachingBubble>
|
||||
@@ -160,12 +165,54 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(6),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 7 of 8"
|
||||
footerContent={"Step 7 of " + totalSteps}
|
||||
>
|
||||
Visualize your data, store queries in an interactive document
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 8:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Launch full screen"
|
||||
target={"#openFullScreenBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(9) : setStep(10)),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(7),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent={"Step 8 of " + totalSteps}
|
||||
>
|
||||
This will open a new tab in your browser to use Cosmos DB Explorer. Using the provided URLs you can share
|
||||
read-write or read-only access with other people.
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 9:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Boost your experience"
|
||||
target={"#freeTierTeachingBubble"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(10),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(8),
|
||||
}}
|
||||
calloutProps={{ directionalHint: DirectionalHint.leftCenter }}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent={"Step 9 of " + totalSteps}
|
||||
>
|
||||
Unlock everything Azure Cosmos DB has to offer When you're ready, upgrade to production.
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 10:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Congratulations!"
|
||||
@@ -180,10 +227,10 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(7),
|
||||
onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(9) : setStep(8)),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 8 of 8"
|
||||
footerContent={"Step " + totalSteps + " of " + totalSteps}
|
||||
>
|
||||
<Stack>
|
||||
<Text style={{ color: "white" }}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { DirectionalHint, Link, Stack, TeachingBubble, Text } from "@fluentui/react";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import React from "react";
|
||||
@@ -17,6 +17,10 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
if (userContext.apiType !== "SQL") {
|
||||
return <></>;
|
||||
}
|
||||
let totalSteps = 8;
|
||||
if (userContext.isTryCosmosDBSubscription) {
|
||||
totalSteps = 9;
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 1:
|
||||
@@ -33,7 +37,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
},
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 1 of 7"
|
||||
footerContent={"Step 1 of " + totalSteps}
|
||||
>
|
||||
Start viewing and working with your data by opening Items under Data
|
||||
</TeachingBubble>
|
||||
@@ -55,7 +59,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(1),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 2 of 7"
|
||||
footerContent={"Step 2 of " + totalSteps}
|
||||
>
|
||||
View item here using the items window. Additionally you can also filter items to be reviewed with the filter
|
||||
function
|
||||
@@ -78,7 +82,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(2),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 3 of 7"
|
||||
footerContent={"Step 3 of " + totalSteps}
|
||||
>
|
||||
Add new item by copy / pasting JSON; or uploading a JSON
|
||||
</TeachingBubble>
|
||||
@@ -98,7 +102,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(3),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 4 of 7"
|
||||
footerContent={"Step 4 of " + totalSteps}
|
||||
>
|
||||
Query your data using either the filter function or new query.
|
||||
</TeachingBubble>
|
||||
@@ -118,7 +122,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(4),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 5 of 7"
|
||||
footerContent={"Step 5 of " + totalSteps}
|
||||
>
|
||||
Change throughput provisioned to your container according to your needs
|
||||
</TeachingBubble>
|
||||
@@ -138,12 +142,54 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
onClick: () => setStep(5),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 6 of 7"
|
||||
footerContent={"Step 6 of " + totalSteps}
|
||||
>
|
||||
Visualize your data, store queries in an interactive document
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 7:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Launch full screen"
|
||||
target={"#openFullScreenBtn"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(8) : setStep(9)),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(6),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent={"Step 7 of " + totalSteps}
|
||||
>
|
||||
This will open a new tab in your browser to use Cosmos DB Explorer. Using the provided URLs you can share
|
||||
read-write or read-only access with other people.
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 8:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Boost your experience"
|
||||
target={"#freeTierTeachingBubble"}
|
||||
hasCloseButton
|
||||
primaryButtonProps={{
|
||||
text: "Next",
|
||||
onClick: () => setStep(9),
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(7),
|
||||
}}
|
||||
calloutProps={{ directionalHint: DirectionalHint.leftCenter }}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent={"Step 8 of " + totalSteps}
|
||||
>
|
||||
Unlock everything Azure Cosmos DB has to offer When you're ready, upgrade to production.
|
||||
</TeachingBubble>
|
||||
);
|
||||
case 9:
|
||||
return (
|
||||
<TeachingBubble
|
||||
headline="Congratulations!"
|
||||
@@ -158,10 +204,10 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
|
||||
}}
|
||||
secondaryButtonProps={{
|
||||
text: "Previous",
|
||||
onClick: () => setStep(6),
|
||||
onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(8) : setStep(7)),
|
||||
}}
|
||||
onDismiss={() => onDimissTeachingBubble()}
|
||||
footerContent="Step 7 of 7"
|
||||
footerContent={"Step " + totalSteps + " of " + totalSteps}
|
||||
>
|
||||
<Stack>
|
||||
<Text style={{ color: "white" }}>
|
||||
|
||||
@@ -14,10 +14,6 @@
|
||||
padding-right: 16px;
|
||||
max-width: 1168px;
|
||||
|
||||
> * {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
> .title {
|
||||
position: relative; // To attach FeaturePanelLauncher as absolute
|
||||
color: @BaseHigh;
|
||||
@@ -40,6 +36,7 @@
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin: 40px auto;
|
||||
width: 84%;
|
||||
|
||||
> .mainButton {
|
||||
min-width: 124px;
|
||||
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
TeachingBubbleContent,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { TerminalKind } from "Contracts/ViewModels";
|
||||
import { useCarousel } from "hooks/useCarousel";
|
||||
import { usePostgres } from "hooks/usePostgres";
|
||||
@@ -91,6 +93,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
() => this.setState({}),
|
||||
(state) => state.showPostgreTeachingBubble
|
||||
),
|
||||
},
|
||||
{
|
||||
dispose: usePostgres.subscribe(
|
||||
() => this.setState({}),
|
||||
(state) => state.showResetPasswordBubble
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -108,8 +116,17 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
<form className="connectExplorerFormContainer">
|
||||
<div className="splashScreenContainer">
|
||||
<div className="splashScreen">
|
||||
<div className="title">
|
||||
{userContext.apiType === "Postgres" ? "Welcome to Cosmos DB - PostgreSQL" : "Welcome to Cosmos DB"}
|
||||
<div
|
||||
className="title"
|
||||
aria-label={
|
||||
userContext.apiType === "Postgres"
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
: "Welcome to Azure Cosmos DB"
|
||||
}
|
||||
>
|
||||
{userContext.apiType === "Postgres"
|
||||
? "Welcome to Azure Cosmos DB for PostgreSQL"
|
||||
: "Welcome to Azure Cosmos DB"}
|
||||
<FeaturePanelLauncher />
|
||||
</div>
|
||||
<div className="subtitle">
|
||||
@@ -118,12 +135,21 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
: "Globally distributed, multi-model database service for any scale"}
|
||||
</div>
|
||||
<div className="mainButtonsContainer">
|
||||
{userContext.apiType === "Postgres" && usePostgres.getState().showPostgreTeachingBubble && (
|
||||
{userContext.apiType === "Postgres" &&
|
||||
usePostgres.getState().showPostgreTeachingBubble &&
|
||||
!usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="New to Cosmos DB PGSQL?"
|
||||
target={"#quickstartDescription"}
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => usePostgres.getState().setShowPostgreTeachingBubble(false)}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.rightCenter,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Get started",
|
||||
onClick: () => {
|
||||
@@ -132,12 +158,13 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
},
|
||||
}}
|
||||
>
|
||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you can
|
||||
find sample data, query.
|
||||
Welcome! If you are new to Cosmos DB PGSQL and need help with getting started, here is where you
|
||||
can find sample data, query.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
{mainItems.map((item) => (
|
||||
<Stack
|
||||
id={`mainButton-${item.id}`}
|
||||
horizontal
|
||||
className="mainButton focusable"
|
||||
key={`${item.title}`}
|
||||
@@ -161,6 +188,36 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</div>
|
||||
</Stack>
|
||||
))}
|
||||
{userContext.apiType === "Postgres" && usePostgres.getState().showResetPasswordBubble && (
|
||||
<TeachingBubble
|
||||
headline="Create your password"
|
||||
target={"#mainButton-quickstartDescription"}
|
||||
hasCloseButton
|
||||
onDismiss={() => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
}}
|
||||
calloutProps={{
|
||||
directionalHint: DirectionalHint.bottomRightEdge,
|
||||
directionalHintFixed: true,
|
||||
preventDismissOnLostFocus: true,
|
||||
preventDismissOnResize: true,
|
||||
preventDismissOnScroll: true,
|
||||
}}
|
||||
primaryButtonProps={{
|
||||
text: "Create",
|
||||
onClick: () => {
|
||||
localStorage.setItem(userContext.databaseAccount.id, "true");
|
||||
sendMessage({
|
||||
type: MessageTypes.OpenPostgreSQLPasswordReset,
|
||||
});
|
||||
usePostgres.getState().setShowResetPasswordBubble(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
If you haven't changed your password yet, change it now.
|
||||
</TeachingBubble>
|
||||
)}
|
||||
</div>
|
||||
{useCarousel.getState().showCoachMark && (
|
||||
<Coachmark
|
||||
@@ -191,8 +248,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</Coachmark>
|
||||
)}
|
||||
{userContext.apiType === "Postgres" ? (
|
||||
<Stack horizontal style={{ margin: "0 auto" }} tokens={{ childrenGap: "15%" }}>
|
||||
<Stack>
|
||||
<Stack horizontal style={{ margin: "0 auto", width: "84%" }} tokens={{ childrenGap: 32 }}>
|
||||
<Stack style={{ width: "33%" }}>
|
||||
<Text
|
||||
variant="large"
|
||||
style={{
|
||||
@@ -204,7 +261,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</Text>
|
||||
{this.getNextStepItems()}
|
||||
</Stack>
|
||||
<Stack>
|
||||
<Stack style={{ width: "33%" }}>
|
||||
<Text
|
||||
variant="large"
|
||||
style={{
|
||||
@@ -216,6 +273,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
</Text>
|
||||
{this.getTipsAndLearnMoreItems()}
|
||||
</Stack>
|
||||
<Stack style={{ width: "33%" }}></Stack>
|
||||
</Stack>
|
||||
) : (
|
||||
<div className="moreStuffContainer">
|
||||
@@ -253,7 +311,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
public createMainItems(): SplashScreenItem[] {
|
||||
const heroes: SplashScreenItem[] = [];
|
||||
|
||||
if (userContext.apiType === "SQL" || userContext.apiType === "Mongo" || userContext.apiType === "Postgres") {
|
||||
if (
|
||||
userContext.apiType === "SQL" ||
|
||||
userContext.apiType === "Mongo" ||
|
||||
(userContext.apiType === "Postgres" && !userContext.isReplica)
|
||||
) {
|
||||
const launchQuickstartBtn = {
|
||||
id: "quickstartDescription",
|
||||
iconSrc: QuickStartIcon,
|
||||
@@ -282,7 +344,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: PowerShellIcon,
|
||||
title: "PostgreSQL Shell",
|
||||
description: "Create table and interact with data using PostgreSQL’s shell interface",
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Mongo),
|
||||
onClick: () => this.container.openNotebookTerminal(TerminalKind.Postgres),
|
||||
};
|
||||
heroes.push(postgreShellBtn);
|
||||
} else {
|
||||
@@ -300,11 +362,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
|
||||
const connectBtn = {
|
||||
iconSrc: ConnectIcon,
|
||||
title: userContext.apiType === "Postgres" ? "Connect with PG Admin" : "Connect",
|
||||
title: userContext.apiType === "Postgres" ? "Connect with pgAdmin" : "Connect",
|
||||
description:
|
||||
userContext.apiType === "Postgres"
|
||||
? "Prefer using your own choice of tooling? Find the connection string you need to connect"
|
||||
: "Prefer PGadmin? Find your connection strings here",
|
||||
? "Prefer pgAdmin? Find your connection strings here"
|
||||
: "Prefer using your own choice of tooling? Find the connection string you need to connect",
|
||||
onClick: () => useTabs.getState().openAndActivateReactTab(ReactTabKind.Connect),
|
||||
};
|
||||
heroes.push(connectBtn);
|
||||
@@ -388,7 +450,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{
|
||||
link: "https://aka.ms/mongodbintro",
|
||||
title: "What is the MongoDB API?",
|
||||
description: "Understand the Cosmos DB API for MongoDB and its features.",
|
||||
description: "Understand Azure Cosmos DB for MongoDB and its features.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/mongodbfeaturesupport",
|
||||
@@ -445,7 +507,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{
|
||||
link: "https://aka.ms/tableintro",
|
||||
title: "What is the Table API?",
|
||||
description: "Understand the Table API in Cosmos DB and its features",
|
||||
description: "Understand Azure Cosmos DB for Table and its features",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tableimport",
|
||||
@@ -454,8 +516,8 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tablefaq",
|
||||
title: "Table API FAQs",
|
||||
description: "Common questions about the Table API",
|
||||
title: "Azure Cosmos DB for Table FAQs",
|
||||
description: "Common questions about Azure Cosmos DB for Table",
|
||||
},
|
||||
];
|
||||
break;
|
||||
@@ -473,7 +535,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
<Image src={LinkIcon} />
|
||||
<Image src={LinkIcon} alt=" " />
|
||||
</Stack>
|
||||
<Text>{item.description}</Text>
|
||||
</Stack>
|
||||
@@ -508,7 +570,17 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
}
|
||||
|
||||
private getLearningResourceItems(): JSX.Element {
|
||||
let items: { link: string; title: string; description: string }[];
|
||||
interface item {
|
||||
link: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
const cdbLiveTv: item = {
|
||||
link: "https://developer.azurecosmosdb.com/tv",
|
||||
title: "Learn the Fundamentals",
|
||||
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
||||
};
|
||||
let items: item[];
|
||||
switch (userContext.apiType) {
|
||||
case "SQL":
|
||||
case "Postgres":
|
||||
@@ -518,11 +590,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Get Started using an SDK",
|
||||
description: "Learn about the Azure Cosmos DB SDK.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/msl-complex-queries",
|
||||
title: "Master Complex Queries",
|
||||
description: "Learn how to author complex queries.",
|
||||
},
|
||||
cdbLiveTv,
|
||||
{
|
||||
link: "https://aka.ms/msl-move-data",
|
||||
title: "Migrate Your Data",
|
||||
@@ -542,11 +610,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Getting Started Guide",
|
||||
description: "Learn the basics to get started.",
|
||||
},
|
||||
{
|
||||
link: "http://aka.ms/mongodotnet",
|
||||
title: "Build a web API",
|
||||
description: "Create a web API with the.NET SDK.",
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
break;
|
||||
case "Cassandra":
|
||||
@@ -556,11 +620,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Create a Container",
|
||||
description: "Get to know the create a container options.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/cassandraserverdiagnostics",
|
||||
title: "Run Server Diagnostics",
|
||||
description: "Learn how to run server diagnostics.",
|
||||
},
|
||||
cdbLiveTv,
|
||||
{
|
||||
link: "https://aka.ms/Cassandrathroughput",
|
||||
title: "Provision Throughput",
|
||||
@@ -580,11 +640,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
title: "Import Graph Data",
|
||||
description: "Learn Bulk ingestion data using BulkExecutor",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/graphoptimize",
|
||||
title: "Optimize your Queries",
|
||||
description: "Learn how to evaluate your Gremlin queries",
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
break;
|
||||
case "Tables":
|
||||
@@ -592,18 +648,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
{
|
||||
link: "https://aka.ms/tabledotnet",
|
||||
title: "Build a .NET App",
|
||||
description: "How to access Table API from a .NET app.",
|
||||
description: "How to access Azure Cosmos DB for Table from a .NET app.",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/Tablejava",
|
||||
title: "Build a Java App",
|
||||
description: "Create a Table API app with Java SDK ",
|
||||
},
|
||||
{
|
||||
link: "https://aka.ms/tablenodejs",
|
||||
title: "Build a Node.js App",
|
||||
description: "Create a Table API app with Node.js SDK",
|
||||
description: "Create a Azure Cosmos DB for Table app with Java SDK ",
|
||||
},
|
||||
cdbLiveTv,
|
||||
];
|
||||
break;
|
||||
}
|
||||
@@ -634,24 +686,24 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private getNextStepItems(): JSX.Element {
|
||||
const items: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "",
|
||||
title: "Performance tuning",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208312",
|
||||
title: "Data Modeling",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "",
|
||||
title: "Join Citus community",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: " https://go.microsoft.com/fwlink/?linkid=2206941 ",
|
||||
title: "How to choose a Distribution Column",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "",
|
||||
title: "Useful diagnostic queries",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207425",
|
||||
title: "Build Apps with Python/Java/Django",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`nextStep${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
@@ -670,24 +722,24 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
private getTipsAndLearnMoreItems(): JSX.Element {
|
||||
const items: { link: string; title: string; description: string }[] = [
|
||||
{
|
||||
link: "",
|
||||
title: "Data modeling",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2207226",
|
||||
title: "Performance Tuning",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "",
|
||||
title: "How to choose a distribution Column",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2208037",
|
||||
title: "Useful Diagnostic Queries",
|
||||
description: "",
|
||||
},
|
||||
{
|
||||
link: "",
|
||||
title: "Build apps with Python/ Java/ Django",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
link: "https://go.microsoft.com/fwlink/?linkid=2205270",
|
||||
title: "Distributed SQL Reference",
|
||||
description: "",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack style={{ minWidth: 124, maxWidth: 296 }}>
|
||||
{items.map((item, i) => (
|
||||
<Stack key={`tips${i}`} style={{ marginBottom: 26 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ fontSize: 14 }}>
|
||||
|
||||
@@ -516,7 +516,7 @@ export default class QueryBuilderViewModel {
|
||||
};
|
||||
|
||||
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
if (event.key === "Enter" || event.key === "Space") {
|
||||
this.addClauseIndex(this.clauseArray().length - 1);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
|
||||
145
src/Explorer/Tabs/PostgresConnectTab.tsx
Normal file
145
src/Explorer/Tabs/PostgresConnectTab.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
Icon,
|
||||
IconButton,
|
||||
IDropdownOption,
|
||||
ITextFieldStyles,
|
||||
Label,
|
||||
Link,
|
||||
Stack,
|
||||
Text,
|
||||
TextField,
|
||||
TooltipHost,
|
||||
} from "@fluentui/react";
|
||||
import React from "react";
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
export const PostgresConnectTab: React.FC = (): JSX.Element => {
|
||||
const { adminLogin, nodes, enablePublicIpAccess } = userContext.postgresConnectionStrParams;
|
||||
const [usePgBouncerPort, setUsePgBouncerPort] = React.useState<boolean>(false);
|
||||
const [selectedNode, setSelectedNode] = React.useState<string>(nodes?.[0]?.value);
|
||||
const portNumber = usePgBouncerPort ? "6432" : "5432";
|
||||
|
||||
const onCopyBtnClicked = (selector: string): void => {
|
||||
const textfield: HTMLInputElement = document.querySelector(selector);
|
||||
textfield.select();
|
||||
document.execCommand("copy");
|
||||
};
|
||||
|
||||
const textfieldStyles: Partial<ITextFieldStyles> = {
|
||||
root: { width: "100%" },
|
||||
field: { backgroundColor: "rgb(230, 230, 230)" },
|
||||
fieldGroup: { borderColor: "rgb(138, 136, 134)" },
|
||||
subComponentStyles: { label: { fontWeight: 400 } },
|
||||
description: { fontWeight: 400 },
|
||||
};
|
||||
|
||||
const nodesDropdownOptions: IDropdownOption[] = nodes.map((node) => ({
|
||||
key: node.value,
|
||||
text: node.text,
|
||||
}));
|
||||
|
||||
const postgresSQLConnectionURL = `postgres://${adminLogin}:{your_password}@${selectedNode}:${portNumber}/citus?sslmode=require`;
|
||||
const psql = `psql "host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require"`;
|
||||
const jdbc = `jdbc:postgresql://${selectedNode}:${portNumber}/citus?user=${adminLogin}&password={your_password}&sslmode=require`;
|
||||
const libpq = `host=${selectedNode} port=${portNumber} dbname=citus user=${adminLogin} password={your_password} sslmode=require`;
|
||||
const adoDotNet = `Server=${selectedNode};Database=citus;Port=${portNumber};User Id=${adminLogin};Password={your_password};Ssl Mode=Require;`;
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", padding: 16 }}>
|
||||
<Stack horizontal verticalAlign="center" style={{ marginBottom: 8 }}>
|
||||
<Label style={{ marginRight: 8 }}>Public IP addresses on worker nodes:</Label>
|
||||
<TooltipHost
|
||||
content="
|
||||
You can enable or disable public IP addresses on the worker nodes on 'Networking' page of your server group."
|
||||
>
|
||||
<Icon style={{ margin: "5px 8px 0 0", cursor: "default" }} iconName="Info" />
|
||||
</TooltipHost>
|
||||
|
||||
<TextField value={enablePublicIpAccess ? "On" : "Off"} readOnly disabled />
|
||||
</Stack>
|
||||
<Stack horizontal style={{ marginBottom: 8 }}>
|
||||
<Label style={{ marginRight: 85 }}>Show connection strings for</Label>
|
||||
<Dropdown
|
||||
options={nodesDropdownOptions}
|
||||
selectedKey={selectedNode}
|
||||
onChange={(_, option) => {
|
||||
const selectedNode = option.key as string;
|
||||
setSelectedNode(selectedNode);
|
||||
if (!selectedNode.startsWith("c.")) {
|
||||
setUsePgBouncerPort(false);
|
||||
}
|
||||
}}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal style={{ marginBottom: 8 }}>
|
||||
<Label style={{ marginRight: 44 }}>PgBouncer connection strings</Label>
|
||||
<Checkbox
|
||||
boxSide="end"
|
||||
checked={usePgBouncerPort}
|
||||
onChange={(_, checked: boolean) => setUsePgBouncerPort(checked)}
|
||||
disabled={!selectedNode?.startsWith("c.")}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="PostgreSQL connection URL"
|
||||
id="postgresSQLConnectionURL"
|
||||
readOnly
|
||||
value={postgresSQLConnectionURL}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#postgresSQLConnectionURL")} />
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField label="psql" id="psql" readOnly value={psql} styles={textfieldStyles} />
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#psql")} />
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField label="JDBC" id="JDBC" readOnly value={jdbc} styles={textfieldStyles} />
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#JDBC")} />
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField
|
||||
label="Node.js, Python, Ruby, PHP, C++ (libpq)"
|
||||
id="libpq"
|
||||
readOnly
|
||||
value={libpq}
|
||||
styles={textfieldStyles}
|
||||
/>
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#libpq")} />
|
||||
</Stack>
|
||||
<Stack horizontal verticalAlign="end" style={{ marginBottom: 8 }}>
|
||||
<TextField label="ADO.NET" id="adoDotNet" readOnly value={adoDotNet} styles={textfieldStyles} />
|
||||
<IconButton iconProps={{ iconName: "Copy" }} onClick={() => onCopyBtnClicked("#adoDotNet")} />
|
||||
</Stack>
|
||||
|
||||
<Label>Secure connections</Label>
|
||||
<Text style={{ marginBottom: 8 }}>
|
||||
Only secure connections are supported. For production use cases, we recommend using the 'verify-full'
|
||||
mode to enforce TLS certificate verification. You will need to download the Hyperscale (Citus) certificate, and
|
||||
provide it when connecting to the database.{" "}
|
||||
<Link href="https://go.microsoft.com/fwlink/?linkid=2155061" target="_blank">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Label>Connect with pgAdmin</Label>
|
||||
<Text>
|
||||
Refer to our{" "}
|
||||
<Link
|
||||
href="https://learn.microsoft.com/en-us/azure/postgresql/hyperscale/howto-connect?tabs=pgadmin"
|
||||
target="_blank"
|
||||
>
|
||||
guide
|
||||
</Link>{" "}
|
||||
to help you connect via pgAdmin.
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -70,24 +70,19 @@
|
||||
<tbody data-bind="template: { name: 'queryClause-template', foreach: clauseArray, as: 'clause' }"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
class="addClause"
|
||||
role="button"
|
||||
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }, attr: { title: addNewClauseLine }"
|
||||
tabindex="0"
|
||||
<button
|
||||
data-bind="click: addNewClause, event: { keydown: onAddNewClauseKeyDown }"
|
||||
style="border: none; background: none"
|
||||
>
|
||||
<div class="addClause" data-bind=" ">
|
||||
<div class="addClause-heading">
|
||||
<span class="clause-table addClause-title">
|
||||
<img
|
||||
class="addclauseProperty-Img"
|
||||
style="margin-bottom: 5px"
|
||||
src="/Add-property.svg"
|
||||
alt="Add new clause"
|
||||
/>
|
||||
<img class="addclauseProperty-Img" style="margin-bottom: 5px" src="/Add-property.svg" />
|
||||
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Tables Query Tab Query Helper - End-->
|
||||
@@ -168,22 +163,20 @@
|
||||
<script type="text/html" id="queryClause-template">
|
||||
<tr class="clause-table-row">
|
||||
<td class="clause-table-cell action-column">
|
||||
<span
|
||||
class="entity-Add-Cancel"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
<button
|
||||
data-bind="click: $parent.addClauseIndex.bind($data, $index()), event: { keydown: $parent.onAddClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.insertNewFilterLine}"
|
||||
>
|
||||
<span class="entity-Add-Cancel" role="button">
|
||||
<img class="querybuilder-addpropertyImg" src="/Add-property.svg" alt="Add clause" />
|
||||
</span>
|
||||
<span
|
||||
class="entity-Add-Cancel"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
</button>
|
||||
<button
|
||||
data-bind="hasFocus: isDeleteButtonFocused, click: $parent.deleteClause.bind($data, $index()), event: { keydown: $parent.onDeleteClauseKeyDown.bind($data, $index()) }, attr:{title: $parent.removeThisFilterLine}"
|
||||
>
|
||||
<span class="entity-Add-Cancel" role="button">
|
||||
<img class="querybuilder-cancelImg" src="/Entity_cancel.svg" alt="Delete clause" />
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="clause-table-cell group-control-column">
|
||||
<input type="checkbox" aria-label="And/Or" data-bind="checked: checkedForGrouping" />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
|
||||
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
|
||||
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { NotebookWorkspaceConnectionInfo, PostgresFirewallRule } from "Contracts/DataModels";
|
||||
import { NotebookTerminalComponent } from "Explorer/Controls/Notebook/NotebookTerminalComponent";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { useNotebook } from "Explorer/Notebook/useNotebook";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import { QuickstartGuide } from "Explorer/Quickstart/QuickstartGuide";
|
||||
import React, { useEffect } from "react";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
|
||||
interface QuickstartTabProps {
|
||||
explorer: Explorer;
|
||||
@@ -13,14 +17,42 @@ interface QuickstartTabProps {
|
||||
|
||||
export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: QuickstartTabProps): JSX.Element => {
|
||||
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
|
||||
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
|
||||
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/postgresql`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
const checkFirewallRules = async (): Promise<void> => {
|
||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response: any = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
});
|
||||
const firewallRules: PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
||||
);
|
||||
setIsAllPublicIPAddressEnabled(isEnabled);
|
||||
|
||||
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
|
||||
if (!isEnabled && useTabs.getState().activeReactTab === ReactTabKind.Quickstart) {
|
||||
setTimeout(checkFirewallRules, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkFirewallRules();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
explorer.allocateContainer();
|
||||
}, []);
|
||||
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
|
||||
authToken: notebookServerInfo.authToken,
|
||||
notebookServerEndpoint: `${notebookServerInfo.notebookServerEndpoint?.replace(/\/+$/, "")}/mongo`,
|
||||
forwardingId: notebookServerInfo.forwardingId,
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack style={{ width: "100%" }} horizontal>
|
||||
@@ -28,15 +60,24 @@ export const QuickstartTab: React.FC<QuickstartTabProps> = ({ explorer }: Quicks
|
||||
<QuickstartGuide />
|
||||
</Stack>
|
||||
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
|
||||
{notebookServerInfo?.notebookServerEndpoint && (
|
||||
{!isAllPublicIPAddressEnabled && <QuickstartFirewallNotification />}
|
||||
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={getNotebookServerInfo()}
|
||||
databaseAccount={userContext.databaseAccount}
|
||||
tabId="EmbbedTerminal"
|
||||
tabId="QuickstartPSQLShell"
|
||||
/>
|
||||
)}
|
||||
{!notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Spinner styles={{ root: { marginTop: 10 } }} size={SpinnerSize.large}></Spinner>
|
||||
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
|
||||
<Stack style={{ margin: "auto 0" }}>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
Connecting to the PostgreSQL shell.
|
||||
</Text>
|
||||
<Text block style={{ margin: "auto" }}>
|
||||
If the cluster was just created, this could take up to a minute.
|
||||
</Text>
|
||||
<Spinner styles={{ root: { marginTop: 16 } }} size={SpinnerSize.large}></Spinner>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { MessageBar, MessageBarButton, MessageBarType } from "@fluentui/react";
|
||||
import { sendMessage } from "Common/MessageHandler";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import { CollectionTabKind } from "Contracts/ViewModels";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { SplashScreen } from "Explorer/SplashScreen/SplashScreen";
|
||||
import { ConnectTab } from "Explorer/Tabs/ConnectTab";
|
||||
import { PostgresConnectTab } from "Explorer/Tabs/PostgresConnectTab";
|
||||
import { QuickstartTab } from "Explorer/Tabs/QuickstartTab";
|
||||
import { useTeachingBubble } from "hooks/useTeachingBubble";
|
||||
import ko from "knockout";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { userContext } from "UserContext";
|
||||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import { useObservable } from "../../hooks/useObservable";
|
||||
@@ -19,10 +24,23 @@ interface TabsProps {
|
||||
}
|
||||
|
||||
export const Tabs = ({ explorer }: TabsProps): JSX.Element => {
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab } = useTabs();
|
||||
const { openedTabs, openedReactTabs, activeTab, activeReactTab, networkSettingsWarning } = useTabs();
|
||||
|
||||
return (
|
||||
<div className="tabsManagerContainer">
|
||||
{networkSettingsWarning && (
|
||||
<MessageBar
|
||||
messageBarType={MessageBarType.warning}
|
||||
actions={
|
||||
<MessageBarButton onClick={() => sendMessage({ type: MessageTypes.OpenPostgresNetworkingBlade })}>
|
||||
Change network settings
|
||||
</MessageBarButton>
|
||||
}
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
>
|
||||
{networkSettingsWarning}
|
||||
</MessageBar>
|
||||
)}
|
||||
<div id="content" className="flexContainer hideOverflows">
|
||||
<div className="nav-tabs-margin">
|
||||
<ul className="nav nav-tabs level navTabHeight" id="navTabs" role="tablist">
|
||||
@@ -74,6 +92,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
|
||||
}
|
||||
}}
|
||||
className={active ? "active tabList" : "tabList"}
|
||||
style={active ? { fontWeight: "bolder" } : {}}
|
||||
title={useObservable(tab?.tabPath || ko.observable(""))}
|
||||
aria-selected={active}
|
||||
aria-expanded={active}
|
||||
@@ -189,7 +208,7 @@ const onKeyPressReactTab = (e: KeyboardEvent, tabKind: ReactTabKind): void => {
|
||||
const getReactTabContent = (activeReactTab: ReactTabKind, explorer: Explorer): JSX.Element => {
|
||||
switch (activeReactTab) {
|
||||
case ReactTabKind.Connect:
|
||||
return <ConnectTab />;
|
||||
return userContext.apiType === "Postgres" ? <PostgresConnectTab /> : <ConnectTab />;
|
||||
case ReactTabKind.Home:
|
||||
return <SplashScreen explorer={explorer} />;
|
||||
case ReactTabKind.Quickstart:
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Spinner, SpinnerSize } from "@fluentui/react";
|
||||
import { configContext } from "ConfigContext";
|
||||
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { armRequest } from "Utils/arm/request";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -26,10 +29,15 @@ class NotebookTerminalComponentAdapter implements ReactAdapter {
|
||||
constructor(
|
||||
private getNotebookServerInfo: () => DataModels.NotebookWorkspaceConnectionInfo,
|
||||
private getDatabaseAccount: () => DataModels.DatabaseAccount,
|
||||
private getTabId: () => string
|
||||
private getTabId: () => string,
|
||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>
|
||||
) {}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (!this.isAllPublicIPAddressesEnabled()) {
|
||||
return <QuickstartFirewallNotification />;
|
||||
}
|
||||
|
||||
return this.parameters() ? (
|
||||
<NotebookTerminalComponent
|
||||
notebookServerInfo={this.getNotebookServerInfo()}
|
||||
@@ -46,25 +54,33 @@ export default class TerminalTab extends TabsBase {
|
||||
public readonly html = '<div style="height: 100%" data-bind="react:notebookTerminalComponentAdapter"></div> ';
|
||||
private container: Explorer;
|
||||
private notebookTerminalComponentAdapter: NotebookTerminalComponentAdapter;
|
||||
private isAllPublicIPAddressesEnabled: ko.Observable<boolean>;
|
||||
|
||||
constructor(options: TerminalTabOptions) {
|
||||
super(options);
|
||||
this.container = options.container;
|
||||
this.isAllPublicIPAddressesEnabled = ko.observable(true);
|
||||
this.notebookTerminalComponentAdapter = new NotebookTerminalComponentAdapter(
|
||||
() => this.getNotebookServerInfo(options),
|
||||
() => userContext?.databaseAccount,
|
||||
() => this.tabId
|
||||
() => this.tabId,
|
||||
this.isAllPublicIPAddressesEnabled
|
||||
);
|
||||
this.notebookTerminalComponentAdapter.parameters = ko.computed<boolean>(() => {
|
||||
if (
|
||||
this.isTemplateReady() &&
|
||||
useNotebook.getState().isNotebookEnabled &&
|
||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint
|
||||
useNotebook.getState().notebookServerInfo?.notebookServerEndpoint &&
|
||||
this.isAllPublicIPAddressesEnabled()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (options.kind === ViewModels.TerminalKind.Postgres) {
|
||||
this.checkPostgresFirewallRules();
|
||||
}
|
||||
}
|
||||
|
||||
public getContainer(): Explorer {
|
||||
@@ -95,6 +111,10 @@ export default class TerminalTab extends TabsBase {
|
||||
endpointSuffix = "cassandra";
|
||||
break;
|
||||
|
||||
case ViewModels.TerminalKind.Postgres:
|
||||
endpointSuffix = "postgresql";
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Terminal kind: ${options.kind} not supported`);
|
||||
}
|
||||
@@ -106,4 +126,25 @@ export default class TerminalTab extends TabsBase {
|
||||
forwardingId: info.forwardingId,
|
||||
};
|
||||
}
|
||||
|
||||
private async checkPostgresFirewallRules(): Promise<void> {
|
||||
const firewallRulesUri = `${userContext.databaseAccount.id}/firewallRules`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const response: any = await armRequest({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: firewallRulesUri,
|
||||
method: "GET",
|
||||
apiVersion: "2020-10-05-privatepreview",
|
||||
});
|
||||
const firewallRules: DataModels.PostgresFirewallRule[] = response?.data?.value || response?.value || [];
|
||||
const isEnabled = firewallRules.some(
|
||||
(rule) => rule.properties.startIpAddress === "0.0.0.0" && rule.properties.endIpAddress === "255.255.255.255"
|
||||
);
|
||||
this.isAllPublicIPAddressesEnabled(isEnabled);
|
||||
|
||||
// If the firewall rule is not added, check every 30 seconds to see if the user has added the rule
|
||||
if (!isEnabled) {
|
||||
setTimeout(() => this.checkPostgresFirewallRules(), 30000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1160,23 +1160,6 @@ export default class Collection implements ViewModels.Collection {
|
||||
this.onDocumentDBDocumentsClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get correct collection label depending on account API
|
||||
*/
|
||||
public getLabel(): string {
|
||||
if (userContext.apiType === "Tables") {
|
||||
return "Entities";
|
||||
} else if (userContext.apiType === "Cassandra") {
|
||||
return "Rows";
|
||||
} else if (userContext.apiType === "Gremlin") {
|
||||
return "Graph";
|
||||
} else if (userContext.apiType === "Mongo") {
|
||||
return "Documents";
|
||||
}
|
||||
|
||||
return "Items";
|
||||
}
|
||||
|
||||
public getDatabase(): ViewModels.Database {
|
||||
return useDatabases.getState().findDatabaseWithId(this.databaseId);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import shallow from "zustand/shallow";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
@@ -497,7 +498,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
const buildCollectionNode = (database: ViewModels.Database, collection: ViewModels.Collection): TreeNode => {
|
||||
const children: TreeNode[] = [];
|
||||
children.push({
|
||||
label: collection.getLabel(),
|
||||
label: getItemName(),
|
||||
id: collection.isSampleCollection ? "sampleItems" : "",
|
||||
onClick: () => {
|
||||
collection.openTab();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { getItemName } from "Utils/APITypeUtils";
|
||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||
import DeleteIcon from "../../../images/delete.svg";
|
||||
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||
@@ -254,7 +255,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
private buildCollectionNode(database: ViewModels.Database, collection: ViewModels.Collection): TreeNode {
|
||||
const children: TreeNode[] = [];
|
||||
children.push({
|
||||
label: collection.getLabel(),
|
||||
label: getItemName(),
|
||||
onClick: () => {
|
||||
collection.openTab();
|
||||
// push to most recent
|
||||
|
||||
@@ -80,6 +80,7 @@ const App: React.FunctionComponent = () => {
|
||||
return (
|
||||
<div className="flexContainer">
|
||||
<div id="divExplorer" className="flexContainer hideOverflows">
|
||||
<div id="freeTierTeachingBubble"> </div>
|
||||
{/* Main Command Bar - Start */}
|
||||
<CommandBar container={explorer} />
|
||||
{/* Collections Tree and Tabs - Begin */}
|
||||
|
||||
@@ -32,6 +32,7 @@ import { userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
|
||||
export class PhoenixClient {
|
||||
private armResourceId: string;
|
||||
private containerHealthHandler: NodeJS.Timeout;
|
||||
private retryOptions: promiseRetry.Options = {
|
||||
retries: Notebook.retryAttempts,
|
||||
@@ -39,8 +40,16 @@ export class PhoenixClient {
|
||||
minTimeout: Notebook.retryAttemptDelayMs,
|
||||
};
|
||||
|
||||
constructor(armResourceId: string) {
|
||||
this.armResourceId = armResourceId;
|
||||
}
|
||||
|
||||
public async allocateContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
return this.executeContainerAssignmentOperation(provisionData, "allocate");
|
||||
return promiseRetry(() => this.executeContainerAssignmentOperation(provisionData, "allocate"), {
|
||||
retries: 4,
|
||||
maxTimeout: 20000,
|
||||
minTimeout: 20000,
|
||||
});
|
||||
}
|
||||
|
||||
public async resetContainer(provisionData: IProvisionData): Promise<IResponse<IPhoenixServiceInfo>> {
|
||||
@@ -75,9 +84,12 @@ export class PhoenixClient {
|
||||
}
|
||||
const phoenixError = responseJson as IPhoenixError;
|
||||
if (response.status === HttpStatusCodes.Forbidden) {
|
||||
throw new Error(this.ConvertToForbiddenErrorString(phoenixError));
|
||||
if (phoenixError.message === "Sequence contains no elements") {
|
||||
throw Error("Phoenix container allocation failed, please try again later.");
|
||||
}
|
||||
throw new Error(phoenixError.message);
|
||||
throw new AbortError(this.ConvertToForbiddenErrorString(phoenixError));
|
||||
}
|
||||
throw new AbortError(phoenixError.message);
|
||||
} catch (error) {
|
||||
error.status = response?.status;
|
||||
throw error;
|
||||
@@ -214,22 +226,21 @@ export class PhoenixClient {
|
||||
}
|
||||
}
|
||||
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint =
|
||||
private getPhoenixControlPlanePathPrefix(): string {
|
||||
if (!this.armResourceId) {
|
||||
throw new Error("The Phoenix client was not initialized properly: missing ARM resourcce id");
|
||||
}
|
||||
|
||||
const toolsEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (!validateEndpoint(phoenixEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
|
||||
if (!validateEndpoint(toolsEndpoint, allowedJunoOrigins)) {
|
||||
const error = `${toolsEndpoint} not allowed as tools endpoint`;
|
||||
console.error(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
return phoenixEndpoint;
|
||||
}
|
||||
|
||||
public getPhoenixControlPlanePathPrefix(): string {
|
||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer/cosmosaccounts${
|
||||
userContext.databaseAccount.id
|
||||
}`;
|
||||
return `${toolsEndpoint}/api/controlplane/toolscontainer/cosmosaccounts${this.armResourceId}`;
|
||||
}
|
||||
|
||||
private static getHeaders(): HeadersInit {
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AuthHeadersUtil {
|
||||
public static async generateEncryptedToken(readOnly: boolean = false): Promise<DataModels.GenerateTokenResponse> {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
|
||||
|
||||
const response = await fetch(url, { method: "POST", headers });
|
||||
const result = await response.json();
|
||||
// This API has a quirk where the response must be parsed to JSON twice
|
||||
return JSON.parse(result);
|
||||
}
|
||||
|
||||
private static _generateResourceUrl(): string {
|
||||
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
|
||||
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
|
||||
const sid = subscriptionId || "";
|
||||
const rg = resourceGroup || "";
|
||||
const dba = databaseAccount?.name || "";
|
||||
const resourceUrl = encodeURIComponent(accountEndpoint);
|
||||
const rid = "";
|
||||
const rtype = "";
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export type Features = {
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly enableThroughputCap: boolean;
|
||||
readonly enablePGQuickstart: boolean;
|
||||
readonly enableHierarchicalKeys: boolean;
|
||||
|
||||
// can be set via both flight and feature flag
|
||||
autoscaleDefault: boolean;
|
||||
@@ -91,7 +91,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
partitionKeyDefault2: "true" === get("pkpartitionkeytest"),
|
||||
notebooksDownBanner: "true" === get("notebooksDownBanner"),
|
||||
enableThroughputCap: "true" === get("enablethroughputcap"),
|
||||
enablePGQuickstart: "true" === get("enablepgquickstart"),
|
||||
enableHierarchicalKeys: "true" === get("enablehierarchicalkeys"),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -40,23 +40,23 @@ export enum SelfServeType {
|
||||
*/
|
||||
export enum BladeType {
|
||||
/**
|
||||
* Keys blade of a SQL API account.
|
||||
* Keys blade of a Azure Cosmos DB for NoSQL account.
|
||||
*/
|
||||
SqlKeys = "keys",
|
||||
/**
|
||||
* Keys blade of a Mongo API account.
|
||||
* Keys blade of a Azure Cosmos DB for MongoDB account.
|
||||
*/
|
||||
MongoKeys = "mongoDbKeys",
|
||||
/**
|
||||
* Keys blade of a Cassandra API account.
|
||||
* Keys blade of a Azure Cosmos DB for Apache Cassandra account.
|
||||
*/
|
||||
CassandraKeys = "cassandraDbKeys",
|
||||
/**
|
||||
* Keys blade of a Gremlin API account.
|
||||
* Keys blade of a Azure Cosmos DB for Apache Gremlin account.
|
||||
*/
|
||||
GremlinKeys = "keys",
|
||||
/**
|
||||
* Keys blade of a Table API account.
|
||||
* Keys blade of a Azure Cosmos DB for Table account.
|
||||
*/
|
||||
TableKeys = "tableKeys",
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* JupyterLab applications based on jupyterLab components
|
||||
*/
|
||||
import { ServerConnection, TerminalManager } from "@jupyterlab/services";
|
||||
import { IMessage } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import { Terminal } from "@jupyterlab/terminal";
|
||||
import { Panel, Widget } from "@phosphor/widgets";
|
||||
import { userContext } from "UserContext";
|
||||
@@ -24,6 +24,10 @@ export class JupyterLabAppFactory {
|
||||
this.isShellStarted = content?.includes("Connected to") && content?.includes("cqlsh");
|
||||
}
|
||||
|
||||
private isPostgresShellStarted(content: string | undefined) {
|
||||
this.isShellStarted = content?.includes("citus=>");
|
||||
}
|
||||
|
||||
constructor(closeTab: () => void) {
|
||||
this.onShellExited = closeTab;
|
||||
this.isShellStarted = false;
|
||||
@@ -36,10 +40,13 @@ export class JupyterLabAppFactory {
|
||||
case "Cassandra":
|
||||
this.checkShellStarted = this.isCassandraShellStarted;
|
||||
break;
|
||||
case "Postgres":
|
||||
this.checkShellStarted = this.isPostgresShellStarted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings) {
|
||||
public async createTerminalApp(serverSettings: ServerConnection.ISettings): Promise<ITerminalConnection | undefined> {
|
||||
const manager = new TerminalManager({
|
||||
serverSettings: serverSettings,
|
||||
});
|
||||
@@ -61,7 +68,7 @@ export class JupyterLabAppFactory {
|
||||
|
||||
if (!term) {
|
||||
console.error("Failed starting terminal");
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
term.title.closable = false;
|
||||
@@ -74,6 +81,9 @@ export class JupyterLabAppFactory {
|
||||
// Attach the widget to the dom.
|
||||
Widget.attach(panel, document.body);
|
||||
|
||||
// Switch focus to the terminal
|
||||
term.activate();
|
||||
|
||||
// Handle resize events.
|
||||
window.addEventListener("resize", () => {
|
||||
panel.update();
|
||||
@@ -83,5 +93,7 @@ export class JupyterLabAppFactory {
|
||||
window.addEventListener("unload", () => {
|
||||
panel.dispose();
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ServerConnection } from "@jupyterlab/services";
|
||||
import { IMessage, ITerminalConnection } from "@jupyterlab/services/lib/terminal/terminal";
|
||||
import "@jupyterlab/terminal/style/index.css";
|
||||
import { MessageTypes } from "Contracts/ExplorerContracts";
|
||||
import postRobot from "post-robot";
|
||||
@@ -41,7 +42,7 @@ const createServerSettings = (props: TerminalProps): ServerConnection.ISettings
|
||||
return ServerConnection.makeSettings(options);
|
||||
};
|
||||
|
||||
const initTerminal = async (props: TerminalProps) => {
|
||||
const initTerminal = async (props: TerminalProps): Promise<ITerminalConnection | undefined> => {
|
||||
// Initialize userContext (only properties which are needed by TelemetryProcessor)
|
||||
updateUserContext({
|
||||
subscriptionId: props.subscriptionId,
|
||||
@@ -55,10 +56,12 @@ const initTerminal = async (props: TerminalProps) => {
|
||||
const startTime = TelemetryProcessor.traceStart(Action.OpenTerminal, data);
|
||||
|
||||
try {
|
||||
await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
const session = await new JupyterLabAppFactory(() => closeTab(props.tabId)).createTerminalApp(serverSettings);
|
||||
TelemetryProcessor.traceSuccess(Action.OpenTerminal, data, startTime);
|
||||
return session;
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(Action.OpenTerminal, data, startTime);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -70,6 +73,7 @@ const closeTab = (tabId: string): void => {
|
||||
};
|
||||
|
||||
const main = async (): Promise<void> => {
|
||||
let session: ITerminalConnection | undefined;
|
||||
postRobot.on(
|
||||
"props",
|
||||
{
|
||||
@@ -80,7 +84,22 @@ const main = async (): Promise<void> => {
|
||||
// Typescript definition for event is wrong. So read props by casting to <any>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const props = (event as any).data as TerminalProps;
|
||||
await initTerminal(props);
|
||||
session = await initTerminal(props);
|
||||
}
|
||||
);
|
||||
|
||||
postRobot.on(
|
||||
"sendMessage",
|
||||
{
|
||||
window: window.parent,
|
||||
domain: window.location.origin,
|
||||
},
|
||||
async (event) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const message = (event as any).data as IMessage;
|
||||
if (session) {
|
||||
session.send(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -26,6 +26,20 @@ export interface CollectionCreationDefaults {
|
||||
throughput: ThroughputDefaults;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
text: string;
|
||||
value: string;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
export interface PostgresConnectionStrParams {
|
||||
adminLogin: string;
|
||||
enablePublicIpAccess: boolean;
|
||||
nodes: Node[];
|
||||
isMarlinServerGroup: boolean;
|
||||
isFreeTier: boolean;
|
||||
}
|
||||
|
||||
interface UserContext {
|
||||
readonly authType?: AuthType;
|
||||
readonly masterKey?: string;
|
||||
@@ -52,6 +66,8 @@ interface UserContext {
|
||||
collectionId: string;
|
||||
partitionKey?: string;
|
||||
};
|
||||
readonly postgresConnectionStrParams?: PostgresConnectionStrParams;
|
||||
readonly isReplica?: boolean;
|
||||
collectionCreationDefaults: CollectionCreationDefaults;
|
||||
}
|
||||
|
||||
@@ -94,14 +110,19 @@ function updateUserContext(newContext: Partial<UserContext>): void {
|
||||
);
|
||||
|
||||
if (!localStorage.getItem(newContext.databaseAccount.id)) {
|
||||
if (newContext.apiType === "Postgres") {
|
||||
if (newContext.isTryCosmosDBSubscription || isNewAccount) {
|
||||
if (newContext.apiType === "Postgres" && !newContext.isReplica) {
|
||||
usePostgres.getState().setShowResetPasswordBubble(true);
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
} else if (userContext.isTryCosmosDBSubscription || isNewAccount) {
|
||||
} else {
|
||||
useCarousel.getState().setShouldOpen(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
traceOpen(Action.OpenCarousel);
|
||||
}
|
||||
} else if (newContext.apiType === "Postgres") {
|
||||
usePostgres.getState().setShowPostgreTeachingBubble(true);
|
||||
localStorage.setItem(newContext.databaseAccount.id, "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
Object.assign(userContext, newContext);
|
||||
@@ -112,10 +133,6 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
return "SQL";
|
||||
}
|
||||
|
||||
if (features.enablePGQuickstart) {
|
||||
return "Postgres";
|
||||
}
|
||||
|
||||
const capabilities = account.properties?.capabilities;
|
||||
if (capabilities) {
|
||||
if (capabilities.find((c) => c.name === "EnableCassandra")) {
|
||||
@@ -134,6 +151,9 @@ function apiType(account: DatabaseAccount | undefined): ApiType {
|
||||
if (account.kind === "MongoDB" || account.kind === "Parse") {
|
||||
return "Mongo";
|
||||
}
|
||||
if (account.kind === "Postgres") {
|
||||
return "Postgres";
|
||||
}
|
||||
return "SQL";
|
||||
}
|
||||
|
||||
|
||||
@@ -57,3 +57,35 @@ export const getUploadName = (): string => {
|
||||
return "Items";
|
||||
}
|
||||
};
|
||||
|
||||
export const getApiShortDisplayName = (): string => {
|
||||
switch (userContext.apiType) {
|
||||
case "Cassandra":
|
||||
return "Apache Cassandra API";
|
||||
case "Gremlin":
|
||||
return "Apache Gremlin API";
|
||||
case "Mongo":
|
||||
return "MongoDB API";
|
||||
case "Postgres":
|
||||
return "PostgreSQL API";
|
||||
case "SQL":
|
||||
return "NoSQL API";
|
||||
case "Tables":
|
||||
return "Table API";
|
||||
}
|
||||
};
|
||||
|
||||
export const getItemName = (): string => {
|
||||
switch (userContext.apiType) {
|
||||
case "Tables":
|
||||
return "Entities";
|
||||
case "Cassandra":
|
||||
return "Rows";
|
||||
case "Gremlin":
|
||||
return "Graph";
|
||||
case "Mongo":
|
||||
return "Documents";
|
||||
default:
|
||||
return "Items";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export function getMsalInstance() {
|
||||
cacheLocation: "localStorage",
|
||||
},
|
||||
auth: {
|
||||
authority: `${configContext.AAD_ENDPOINT}common`,
|
||||
authority: `${configContext.AAD_ENDPOINT}organizations`,
|
||||
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -21,6 +21,23 @@ function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEven
|
||||
return false;
|
||||
}
|
||||
|
||||
export function shouldProcessMessage(event: MessageEvent): boolean {
|
||||
if (typeof event.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (event.data["signature"] !== "pcIframe") {
|
||||
return false;
|
||||
}
|
||||
if (!("data" in event.data)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof event.data["data"] !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isReadyMessage(event: MessageEvent): boolean {
|
||||
if (!event?.data?.kind && !event?.data?.data) {
|
||||
return false;
|
||||
|
||||
46
src/Utils/NetworkUtility.ts
Normal file
46
src/Utils/NetworkUtility.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { userContext } from "UserContext";
|
||||
|
||||
const PortalIPs: { [key: string]: string[] } = {
|
||||
prod1: ["104.42.195.92", "40.76.54.131"],
|
||||
prod2: ["104.42.196.69"],
|
||||
mooncake: ["139.217.8.252"],
|
||||
blackforest: ["51.4.229.218"],
|
||||
fairfax: ["52.244.48.71"],
|
||||
ussec: ["29.26.26.67", "29.26.26.66"],
|
||||
usnat: ["7.28.202.68"],
|
||||
};
|
||||
|
||||
export const getNetworkSettingsWarningMessage = (): string => {
|
||||
const accountProperties = userContext.databaseAccount?.properties;
|
||||
|
||||
if (!accountProperties) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// public network access is disabled
|
||||
if (accountProperties.publicNetworkAccess !== "Enabled") {
|
||||
return "The Network settings for this account are preventing access from Data Explorer. Please enable public access to proceed.";
|
||||
}
|
||||
|
||||
const ipRules = accountProperties.ipRules;
|
||||
// public network access is set to "All networks"
|
||||
if (ipRules.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (userContext.apiType === "Cassandra" || userContext.apiType === "Mongo") {
|
||||
const portalIPs = PortalIPs[userContext.portalEnv];
|
||||
let numberOfMatches = 0;
|
||||
ipRules.forEach((ipRule) => {
|
||||
if (portalIPs.indexOf(ipRule.ipAddressOrRange) !== -1) {
|
||||
numberOfMatches++;
|
||||
}
|
||||
});
|
||||
|
||||
if (numberOfMatches !== portalIPs.length) {
|
||||
return "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { GenerateTokenResponse } from "../Contracts/DataModels";
|
||||
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
|
||||
|
||||
export function useFullScreenURLs(): GenerateTokenResponse | undefined {
|
||||
const [state, setState] = useState<GenerateTokenResponse>();
|
||||
|
||||
useEffect(() => {
|
||||
Promise.all([AuthHeadersUtil.generateEncryptedToken(), AuthHeadersUtil.generateEncryptedToken(true)]).then(
|
||||
([readWriteResponse, readOnlyResponse]) =>
|
||||
setState({
|
||||
readWrite: readWriteResponse.readWrite,
|
||||
read: readOnlyResponse.read,
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useTabs } from "hooks/useTabs";
|
||||
import Explorer from "Explorer/Explorer";
|
||||
import { ReactTabKind, useTabs } from "hooks/useTabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
|
||||
import { applyExplorerBindings } from "../applyExplorerBindings";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { AccountKind, Flights } from "../Common/Constants";
|
||||
@@ -9,7 +11,6 @@ import { configContext, Platform, updateConfigContext } from "../ConfigContext";
|
||||
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
|
||||
import { useDatabases } from "../Explorer/useDatabases";
|
||||
import {
|
||||
@@ -28,11 +29,11 @@ import {
|
||||
} from "../Platform/Hosted/HostedUtils";
|
||||
import { CollectionCreation } from "../Shared/Constants";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
import { PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
|
||||
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
|
||||
import { getMsalInstance } from "../Utils/AuthorizationUtils";
|
||||
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
|
||||
import { isInvalidParentFrameOrigin, shouldProcessMessage } from "../Utils/MessageValidation";
|
||||
|
||||
// This hook will create a new instance of Explorer.ts and bind it to the DOM
|
||||
// This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
|
||||
@@ -100,8 +101,12 @@ async function configureHosted(): Promise<Explorer> {
|
||||
}
|
||||
|
||||
if (event.data?.type === MessageTypes.CloseTab) {
|
||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||
} else {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -234,6 +239,7 @@ async function configurePortal(): Promise<Explorer> {
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
});
|
||||
let explorer: Explorer;
|
||||
return new Promise((resolve) => {
|
||||
// In development mode, try to load the iframe message from session storage.
|
||||
// This allows webpack hot reload to function properly in the portal
|
||||
@@ -246,7 +252,7 @@ async function configurePortal(): Promise<Explorer> {
|
||||
);
|
||||
console.dir(message);
|
||||
updateContextsFromPortalMessage(message);
|
||||
const explorer = new Explorer();
|
||||
explorer = new Explorer();
|
||||
// In development mode, save the iframe message from the portal in session storage.
|
||||
// This allows webpack hot reload to funciton properly
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
@@ -282,7 +288,7 @@ async function configurePortal(): Promise<Explorer> {
|
||||
}
|
||||
|
||||
updateContextsFromPortalMessage(inputs);
|
||||
const explorer = new Explorer();
|
||||
explorer = new Explorer();
|
||||
resolve(explorer);
|
||||
if (openAction) {
|
||||
handleOpenAction(openAction, useDatabases.getState().databases, explorer);
|
||||
@@ -290,8 +296,14 @@ async function configurePortal(): Promise<Explorer> {
|
||||
} else if (shouldForwardMessage(message, event.origin)) {
|
||||
sendMessage(message);
|
||||
} else if (event.data?.type === MessageTypes.CloseTab) {
|
||||
if (event.data?.data?.tabId === "QuickstartPSQLShell") {
|
||||
useTabs.getState().closeReactTab(ReactTabKind.Quickstart);
|
||||
} else {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
|
||||
}
|
||||
} else if (message?.type === MessageTypes.RefreshResources) {
|
||||
explorer.onRefreshResourcesClick();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
@@ -305,23 +317,6 @@ function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {
|
||||
return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo;
|
||||
}
|
||||
|
||||
function shouldProcessMessage(event: MessageEvent): boolean {
|
||||
if (typeof event.data !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (event.data["signature"] !== "pcIframe") {
|
||||
return false;
|
||||
}
|
||||
if (!("data" in event.data)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof event.data["data"] !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
if (
|
||||
configContext.BACKEND_ENDPOINT &&
|
||||
@@ -354,9 +349,32 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
|
||||
collectionCreationDefaults: inputs.defaultCollectionThroughput,
|
||||
isTryCosmosDBSubscription: inputs.isTryCosmosDBSubscription,
|
||||
});
|
||||
|
||||
if (inputs.isPostgresAccount) {
|
||||
updateUserContext({ apiType: "Postgres", isReplica: !!inputs.isReplica });
|
||||
|
||||
if (inputs.connectionStringParams) {
|
||||
// TODO: Remove after the nodes param has been updated to be a flat array in the OSS extension
|
||||
let flattenedNodesArray: Node[] = [];
|
||||
inputs.connectionStringParams.nodes?.forEach((node: Node | Node[]) => {
|
||||
if (Array.isArray(node)) {
|
||||
flattenedNodesArray = [...flattenedNodesArray, ...node];
|
||||
} else {
|
||||
flattenedNodesArray.push(node);
|
||||
}
|
||||
});
|
||||
inputs.connectionStringParams.nodes = flattenedNodesArray;
|
||||
updateUserContext({ postgresConnectionStrParams: inputs.connectionStringParams });
|
||||
}
|
||||
}
|
||||
|
||||
const warningMessage = getNetworkSettingsWarningMessage();
|
||||
useTabs.getState().setNetworkSettingsWarning(warningMessage);
|
||||
|
||||
if (inputs.features) {
|
||||
Object.assign(userContext.features, extractFeatures(new URLSearchParams(inputs.features)));
|
||||
}
|
||||
|
||||
if (inputs.flights) {
|
||||
if (inputs.flights.indexOf(Flights.AutoscaleTest) !== -1) {
|
||||
userContext.features.autoscaleDefault;
|
||||
|
||||
@@ -2,10 +2,14 @@ import create, { UseStore } from "zustand";
|
||||
|
||||
interface TeachingBubbleState {
|
||||
showPostgreTeachingBubble: boolean;
|
||||
showResetPasswordBubble: boolean;
|
||||
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => void;
|
||||
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => void;
|
||||
}
|
||||
|
||||
export const usePostgres: UseStore<TeachingBubbleState> = create((set) => ({
|
||||
showPostgreTeachingBubble: false,
|
||||
showResetPasswordBubble: false,
|
||||
setShowPostgreTeachingBubble: (showPostgreTeachingBubble: boolean) => set({ showPostgreTeachingBubble }),
|
||||
setShowResetPasswordBubble: (showResetPasswordBubble: boolean) => set({ showResetPasswordBubble }),
|
||||
}));
|
||||
|
||||
@@ -9,6 +9,7 @@ interface TabsState {
|
||||
openedReactTabs: ReactTabKind[];
|
||||
activeTab: TabsBase | undefined;
|
||||
activeReactTab: ReactTabKind | undefined;
|
||||
networkSettingsWarning: string;
|
||||
activateTab: (tab: TabsBase) => void;
|
||||
activateNewTab: (tab: TabsBase) => void;
|
||||
activateReactTab: (tabkind: ReactTabKind) => void;
|
||||
@@ -20,6 +21,7 @@ interface TabsState {
|
||||
closeAllNotebookTabs: (hardClose: boolean) => void;
|
||||
openAndActivateReactTab: (tabKind: ReactTabKind) => void;
|
||||
closeReactTab: (tabKind: ReactTabKind) => void;
|
||||
setNetworkSettingsWarning: (warningMessage: string) => void;
|
||||
}
|
||||
|
||||
export enum ReactTabKind {
|
||||
@@ -33,6 +35,7 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
openedReactTabs: [ReactTabKind.Home],
|
||||
activeTab: undefined,
|
||||
activeReactTab: ReactTabKind.Home,
|
||||
networkSettingsWarning: "",
|
||||
activateTab: (tab: TabsBase): void => {
|
||||
if (get().openedTabs.some((openedTab) => openedTab.tabId === tab.tabId)) {
|
||||
set({ activeTab: tab, activeReactTab: undefined });
|
||||
@@ -142,4 +145,5 @@ export const useTabs: UseStore<TabsState> = create((set, get) => ({
|
||||
|
||||
set({ openedReactTabs: updatedOpenedReactTabs });
|
||||
},
|
||||
setNetworkSettingsWarning: (warningMessage: string) => set({ networkSettingsWarning: warningMessage }),
|
||||
}));
|
||||
|
||||
26
src/hooks/useTerminal.ts
Normal file
26
src/hooks/useTerminal.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import postRobot from "post-robot";
|
||||
import create, { UseStore } from "zustand";
|
||||
|
||||
interface TerminalState {
|
||||
terminalWindow: Window;
|
||||
setTerminal: (terminalWindow: Window) => void;
|
||||
sendMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
export const useTerminal: UseStore<TerminalState> = create((set, get) => ({
|
||||
terminalWindow: undefined,
|
||||
setTerminal: (terminalWindow: Window) => {
|
||||
set({ terminalWindow });
|
||||
},
|
||||
sendMessage: (message: string) => {
|
||||
const terminalWindow = get().terminalWindow;
|
||||
postRobot.send(
|
||||
terminalWindow,
|
||||
"sendMessage",
|
||||
{ type: "stdin", content: [message] },
|
||||
{
|
||||
domain: window.location.origin,
|
||||
}
|
||||
);
|
||||
},
|
||||
}));
|
||||
@@ -82,7 +82,6 @@
|
||||
"./src/Explorer/Tree/AccessibleVerticalList.ts",
|
||||
"./src/GitHub/GitHubConnector.ts",
|
||||
"./src/HostedExplorerChildFrame.ts",
|
||||
"./src/Platform/Hosted/Authorization.ts",
|
||||
"./src/Platform/Hosted/Components/MeControl.test.tsx",
|
||||
"./src/Platform/Hosted/Components/MeControl.tsx",
|
||||
"./src/Platform/Hosted/Components/SignInButton.tsx",
|
||||
@@ -126,7 +125,6 @@
|
||||
"./src/Utils/WindowUtils.ts",
|
||||
"./src/hooks/useConfig.ts",
|
||||
"./src/hooks/useDirectories.tsx",
|
||||
"./src/hooks/useFullScreenURLs.tsx",
|
||||
"./src/hooks/useGraphPhoto.tsx",
|
||||
"./src/hooks/useNotebookSnapshotStore.ts",
|
||||
"./src/hooks/usePortalAccessToken.tsx",
|
||||
|
||||
Reference in New Issue
Block a user