Compare commits

..

34 Commits

Author SHA1 Message Date
MokireddySampath
b32d1b335c Merge branch 'master' into solution2270063 2023-04-04 22:58:16 +05:30
MokireddySampath
c7c894d6d9 Sampath accessibility sev 2 2 (#1404)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Accessibility sev-2 defects-2

* corrections for 2278347,2278096 and fix for 2264174

* color for placeholder changed to 767474, margin is set to accommodate height between treeheader elements

* padding added for databaseheader, removed margin and restored padding to treenodeheader
2023-04-03 22:11:40 +05:30
MokireddySampath
12310b7208 Merge branch 'master' into solution2270063 2023-03-31 20:11:14 +05:30
Sampath
e55c94f172 solution for defect2270063 2023-03-31 20:09:33 +05:30
Sampath
ef1af8a1ca format change for the file 2023-03-31 20:00:50 +05:30
Sampath
b888a1d578 aria-hidden added for image which is present for presentation purpose 2023-03-31 19:52:14 +05:30
Sampath
b246f408f4 descriptive alt added for iconspresent for DATA and NOTEBOOK accordion headers 2023-03-31 19:02:12 +05:30
Sampath
b71e8746dc wrong role is assigned to anchor link 2023-03-31 18:20:52 +05:30
Sampath
1883c27553 refresh, expand, collapse tree does not have proper label for screenreader 2023-03-31 17:48:35 +05:30
Sampath
a8298bf1fa wrong aria attibute used 2023-03-31 15:06:21 +05:30
Asier Isayas
547954c3dc Lazy loading containers (#1411)
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2023-03-30 14:53:36 -07:00
Sampath
dedb9cab86 fix for defect 2276938 2023-03-29 22:03:33 +05:30
Sampath
e5a4ae2803 Merge branch 'accessibility-sev-3-defects' of https://github.com/MokireddySampath/cosmos-explorer into accessibility-sev-3-defects 2023-03-29 21:37:36 +05:30
Sampath
da2ba243fb corrections for 2278347,2278096 and fix for 2264174 2023-03-29 21:36:39 +05:30
sindhuba
7f220bf8be Add additional teaching bubbles in Quickstart (#1407)
* Add additional teaching bubbles in Quickstart

* Run npm format

* Fix lint error

* Add unit tests

* Add Mongo teaching bubbles for Try CosmosDB and Launch full screen

* Add additional tests for UrlUtility

* Run npm format

* Add tests for Notebook Utils
2023-03-27 15:33:55 -07:00
jawelton74
1ee6abf890 Change AAD endpoint from /common to /organizations. (#1408) 2023-03-27 10:47:04 -07:00
Sampath
e2b72a3824 corrections for 2278347,2278096 and fix for 2264174 2023-03-27 15:22:43 +05:30
Sampath
f27706bc37 Accessibility sev-2 defects-2 2023-03-20 13:09:04 +05:30
MokireddySampath
72c3605dbe Sampath accessibility sev 2 (#1400)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images

* sev2 accessibilitydefects in data explorer

* Revert "sev2 accessibilitydefects in data explorer"

This reverts commit b84d5b572c.

* Sev2 accessibilitydefects

* Revert "Sev2 accessibilitydefects"

This reverts commit a4e60f106c.

* accessibilitydefects-data explorer

* Remove extra white space

---------

Co-authored-by: Victor Meng <vimeng@microsoft.com>
2023-03-14 23:49:21 +05:30
Sampath
fa81442222 accessibilitydefects-data explorer 2023-03-13 18:14:20 +05:30
Sampath
25ef86a6c0 Revert "Sev2 accessibilitydefects"
This reverts commit a4e60f106c.
2023-03-13 17:11:43 +05:30
Sampath
a4e60f106c Sev2 accessibilitydefects 2023-03-13 16:54:24 +05:30
Sampath
d646686723 Revert "sev2 accessibilitydefects in data explorer"
This reverts commit b84d5b572c.
2023-03-13 14:56:51 +05:30
Sampath
b84d5b572c sev2 accessibilitydefects in data explorer 2023-03-10 23:22:42 +05:30
MokireddySampath
a7a38807df Defect2236159 (#1396)
* autoscale and manual radiobuutton fixes

* alt text attribute for images

* Revert "alt text attribute for images"

This reverts commit 5a660551c6.

* alt text for decorative images
2023-02-28 09:11:52 +05:30
Sampath
c711b59f7d alt text for decorative images 2023-02-25 23:25:12 +05:30
Sampath
e945963cf9 Revert "alt text attribute for images"
This reverts commit 5a660551c6.
2023-02-25 23:11:20 +05:30
sindhuba
1285ffc440 Refresh collection automatically when container is created using Quickstart pop up (#1394)
* Quickstart Refresh collection automatically when container is created

* Fix unit tests

* Fix unit tests and address comments

* Minor fixes in message handler

* Minor changes to fix tsconfig.strict.json

* Resolve npm compile:strict errors by fixing the code implementation

* Remove cache refresh code in configureHosted function

* Fix spacing

* Run npm format
2023-02-24 10:58:36 -08:00
jawelton74
b4bca3d41a Updates user to use for nuget push. (#1393) 2023-02-16 17:51:57 -08:00
jawelton74
2a47e4311c Set overwrite to true for both blob uploads to storage. (#1392) 2023-02-16 16:23:24 -08:00
bogercraig
49d9f489d8 Users/bogercraig/add cdb live show link (#1391)
* Exchanged links on DE home page with link to the Azure Cosmos DB Live TV show.

* Adding back accidentally removed terminating ,

* Added cdb TV to Postgres quick start splash page.

* Removing cdb tv description from next steps and moved up.

* Moved cdb tv to the tips and learn more column on postgres getting started page.

* Shortening postgrest cdb tv line.

* Removing link from PG since only 1 episode so far for PG.

* Adding terminating comma back.  Formatting.

* Consolidating Cosmos DB Live TV to a single variable.

* Updating prettier formatting.
2023-02-15 09:24:55 -05:00
sindhuba
31cc129aa7 Update address sample data for SQL and Mongo (#1389) 2023-02-07 18:34:10 -08:00
MokireddySampath
99af4acca4 autoscale and manual radiobuutton fixes (#1387) 2023-02-07 11:48:45 +05:30
victor-meng
80dd161a6f Remove networking warning message for sql, gremlin, and tables api (#1388) 2023-02-06 12:51:04 -08:00
47 changed files with 6984 additions and 339 deletions

View File

@@ -92,11 +92,11 @@ jobs:
name: dist name: dist
path: dist/ path: dist/
- name: Upload build to preview blob storage - 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: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
- name: Upload preview config to blob storage - 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: env:
PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }} PREVIEW_STORAGE_KEY: ${{ secrets.PREVIEW_STORAGE_KEY }}
endtoendemulator: endtoendemulator:
@@ -182,7 +182,7 @@ jobs:
with: with:
name: dist name: dist
- run: cp ./configs/prod.json config.json - run: cp ./configs/prod.json config.json
- run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "vimeng@microsoft.com" -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 pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
@@ -207,7 +207,7 @@ jobs:
name: dist name: dist
- run: cp ./configs/mpac.json config.json - 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: 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 "vimeng@microsoft.com" -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 pack -Version "2.0.0-github-${GITHUB_SHA}"
- run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg - run: nuget push -SkipDuplicate -Source "$NUGET_SOURCE" -ApiKey Az *.nupkg
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" ?> <?xml version="1.0" ?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'> <!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"> <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> </svg>

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 649 B

View File

@@ -2576,6 +2576,10 @@ a:link {
.querydropdown.placeholderVisible { .querydropdown.placeholderVisible {
font-style: italic; font-style: italic;
} }
.querydropdown.placeholderVisible::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: #767474;
opacity: 1;
}
.querydropdown:hover { .querydropdown:hover {
background-color: @AccentLow; background-color: @AccentLow;
@@ -3087,3 +3091,4 @@ a:link {
background: white; background: white;
height: 100%; height: 100%;
} }

View File

@@ -5,12 +5,17 @@
overflow: auto; overflow: auto;
.databaseHeader { .databaseHeader {
padding: 1px;
font-size: 14px; font-size: 14px;
} }
.collectionHeader { .collectionHeader {
font-size: 12px; font-size: 12px;
} }
.loadMoreHeader {
color: RGB(5, 99, 193);
}
} }
.notebookResourceTree { .notebookResourceTree {
@@ -23,6 +28,4 @@
.clickDisabled { .clickDisabled {
pointer-events: none; pointer-events: none;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
id="collapseToggleLeftPaneButton" id="collapseToggleLeftPaneButton"
role="button" role="button"
tabIndex={0} tabIndex={0}
aria-label="Expand Tree" aria-label={getApiShortDisplayName() + `Expand tree`}
onClick={toggleLeftPaneExpanded} onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded} onKeyPress={onKeyPressToggleLeftPaneExpanded}
ref={focusButton} ref={focusButton}

View File

@@ -141,7 +141,7 @@ export class Queries {
public static UnlimitedPageOption: string = "unlimited"; public static UnlimitedPageOption: string = "unlimited";
public static itemsPerPage: number = 100; public static itemsPerPage: number = 100;
public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions public static unlimitedItemsPerPage: number = 100; // TODO: Figure out appropriate value so it works for accounts with a large number of partitions
public static containersPerPage: number = 50;
public static QueryEditorMinHeightRatio: number = 0.1; public static QueryEditorMinHeightRatio: number = 0.1;
public static QueryEditorMaxHeightRatio: number = 0.4; public static QueryEditorMaxHeightRatio: number = 0.4;
public static readonly DefaultMaxDegreeOfParallelism = 6; public static readonly DefaultMaxDegreeOfParallelism = 6;

View 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);
});
});

View File

@@ -1,6 +1,6 @@
import * as OfferUtility from "./OfferUtility";
import { SDKOfferDefinition, Offer } from "../Contracts/DataModels";
import { OfferResponse } from "@azure/cosmos"; import { OfferResponse } from "@azure/cosmos";
import { Offer, SDKOfferDefinition } from "../Contracts/DataModels";
import * as OfferUtility from "./OfferUtility";
describe("parseSDKOfferResponse", () => { describe("parseSDKOfferResponse", () => {
it("manual throughput", () => { it("manual throughput", () => {
@@ -31,6 +31,26 @@ describe("parseSDKOfferResponse", () => {
expect(OfferUtility.parseSDKOfferResponse(mockResponse)).toEqual(expectedResult); 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", () => { it("autoscale throughput", () => {
const mockOfferDefinition = { const mockOfferDefinition = {
content: { content: {

View File

@@ -51,7 +51,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
role="button" role="button"
data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }" data-bind="click: onRefreshResourcesClick, clickBubble: false, event: { keypress: onRefreshDatabasesKeyPress }"
tabIndex={0} tabIndex={0}
aria-label="Refresh tree" aria-label={getApiShortDisplayName() + `Refresh tree`}
title="Refresh tree" title="Refresh tree"
> >
<img className="refreshcol" src={refreshImg} alt="Refresh Tree" /> <img className="refreshcol" src={refreshImg} alt="Refresh Tree" />
@@ -63,7 +63,7 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
onClick={toggleLeftPaneExpanded} onClick={toggleLeftPaneExpanded}
onKeyPress={onKeyPressToggleLeftPaneExpanded} onKeyPress={onKeyPressToggleLeftPaneExpanded}
tabIndex={0} tabIndex={0}
aria-label="Collapse Tree" aria-label={getApiShortDisplayName() + `Collapse Tree`}
title="Collapse Tree" title="Collapse Tree"
ref={focusButton} ref={focusButton}
> >

View File

@@ -137,15 +137,16 @@ export const TableEntity: FunctionComponent<TableEntityProps> = ({
/> />
{!isEntityValueDisable && ( {!isEntityValueDisable && (
<TooltipHost content="Edit property" id="editTooltip"> <TooltipHost content="Edit property" id="editTooltip">
<Image <div tabIndex={0}>
{...imageProps} <Image
src={EditIcon} {...imageProps}
alt="editEntity" src={EditIcon}
id="editEntity" alt="editEntity"
onClick={onEditEntity} onClick={onEditEntity}
tabIndex={0} tabIndex={0}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
/> />
</div>
</TooltipHost> </TooltipHost>
)} )}
{isDeleteOptionVisible && userContext.apiType !== "Cassandra" && ( {isDeleteOptionVisible && userContext.apiType !== "Cassandra" && (

View 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");
});
});

View File

@@ -1,3 +1,4 @@
import { Queries } from "Common/Constants";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -31,6 +32,35 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
} }
} }
export async function readCollectionsWithPagination(
databaseId: string,
continuationToken?: string
): Promise<DataModels.CollectionsWithPagination> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
try {
const sdkResponse = await client()
.database(databaseId)
.containers.query(
{ query: "SELECT * FROM c" },
{
continuationToken,
maxItemCount: Queries.containersPerPage,
}
)
.fetchNext();
const collectionsWithPagination: DataModels.CollectionsWithPagination = {
collections: sdkResponse.resources as DataModels.Collection[],
continuationToken: sdkResponse.continuationToken,
};
return collectionsWithPagination;
} catch (error) {
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {
clearMessage();
}
}
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> { async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
let rpResponse; let rpResponse;

View File

@@ -156,6 +156,11 @@ export interface Collection extends Resource {
requestSchema?: () => void; requestSchema?: () => void;
} }
export interface CollectionsWithPagination {
continuationToken?: string;
collections?: Collection[];
}
export interface Database extends Resource { export interface Database extends Resource {
collections?: Collection[]; collections?: Collection[];
} }

View File

@@ -87,13 +87,13 @@ export interface Database extends TreeNode {
isDatabaseExpanded: ko.Observable<boolean>; isDatabaseExpanded: ko.Observable<boolean>;
isDatabaseShared: ko.Computed<boolean>; isDatabaseShared: ko.Computed<boolean>;
isSampleDB?: boolean; isSampleDB?: boolean;
collectionsContinuationToken?: string;
selectedSubnodeKind: ko.Observable<CollectionTabKind>; selectedSubnodeKind: ko.Observable<CollectionTabKind>;
expandDatabase(): Promise<void>; expandDatabase(): Promise<void>;
collapseDatabase(): void; collapseDatabase(): void;
loadCollections(): Promise<void>; loadCollections(restart?: boolean): Promise<void>;
findCollectionWithId(collectionId: string): Collection; findCollectionWithId(collectionId: string): Collection;
openAddCollection(database: Database, event: MouseEvent): void; openAddCollection(database: Database, event: MouseEvent): void;
onSettingsClick: () => void; onSettingsClick: () => void;

View File

@@ -73,7 +73,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
<img <img
className="expandCollapseIcon" className="expandCollapseIcon"
src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon} src={this.state.isExpanded ? TriangleDownIcon : TriangleRightIcon}
alt="Hide" alt={this.state.isExpanded ? `${this.props.title} hide` : `${this.props.title} expand`}
tabIndex={0} tabIndex={0}
role="button" role="button"
/> />

View File

@@ -46,10 +46,13 @@ export class TabComponent extends React.Component<TabComponentProps> {
} }
let className = "toggleSwitch"; let className = "toggleSwitch";
let ariaselected;
if (index === this.props.currentTabIndex) { if (index === this.props.currentTabIndex) {
className += " selectedToggle"; className += " selectedToggle";
ariaselected = true;
} else { } else {
className += " unselectedToggle"; className += " unselectedToggle";
ariaselected = false;
} }
return ( return (
@@ -57,9 +60,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
<AccessibleElement <AccessibleElement
as="span" as="span"
className={className} className={className}
role="presentation" role="tab"
onActivated={() => this.setActiveTab(index)} onActivated={() => this.setActiveTab(index)}
aria-label={`Select tab: ${tab.title}`} aria-label={`Select tab: ${tab.title}`}
aria-selected={ariaselected}
> >
{tab.title} {tab.title}
</AccessibleElement> </AccessibleElement>
@@ -77,7 +81,11 @@ export class TabComponent extends React.Component<TabComponentProps> {
return ( return (
<div className="tabComponentContainer"> <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 className={className}>{currentTabContent.render()}</div>
</div> </div>
); );

View File

@@ -185,35 +185,37 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
</Stack> </Stack>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<input <div role="radiogroup">
id="Autoscale-input" <input
className="throughputInputRadioBtn" id="Autoscale-input"
aria-label="Autoscale database throughput" className="throughputInputRadioBtn"
aria-required={true} aria-label="Autoscale database throughput"
checked={isAutoscaleSelected} aria-required={true}
type="radio" checked={isAutoscaleSelected}
role="radio" type="radio"
tabIndex={0} role="radio"
onChange={(e) => handleOnChangeMode(e, "Autoscale")} tabIndex={0}
/> onChange={(e) => handleOnChangeMode(e, "Autoscale")}
<label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel"> />
Autoscale <label htmlFor="Autoscale-input" className="throughputInputRadioBtnLabel">
</label> Autoscale
</label>
<input <input
id="Manual-input" id="Manual-input"
className="throughputInputRadioBtn" className="throughputInputRadioBtn"
aria-label="Manual database throughput" aria-label="Manual database throughput"
checked={!isAutoscaleSelected} checked={!isAutoscaleSelected}
type="radio" type="radio"
aria-required={true} aria-required={true}
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={(e) => handleOnChangeMode(e, "Manual")} onChange={(e) => handleOnChangeMode(e, "Manual")}
/> />
<label className="throughputInputRadioBtnLabel" htmlFor="Manual-input"> <label className="throughputInputRadioBtnLabel" htmlFor="Manual-input">
Manual Manual
</label> </label>
</div>
</Stack> </Stack>
{isAutoscaleSelected && ( {isAutoscaleSelected && (

View File

@@ -654,44 +654,45 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
<div <div
className="ms-Stack css-58" className="ms-Stack css-58"
> >
<input <div
aria-label="Autoscale database throughput"
aria-required={true}
checked={true}
className="throughputInputRadioBtn"
id="Autoscale-input"
key=".0:$.0" key=".0:$.0"
onChange={[Function]} role="radiogroup"
role="radio"
tabIndex={0}
type="radio"
/>
<label
className="throughputInputRadioBtnLabel"
htmlFor="Autoscale-input"
key=".0:$.1"
> >
Autoscale <input
</label> aria-label="Autoscale database throughput"
<input aria-required={true}
aria-label="Manual database throughput" checked={true}
aria-required={true} className="throughputInputRadioBtn"
checked={false} id="Autoscale-input"
className="throughputInputRadioBtn" onChange={[Function]}
id="Manual-input" role="radio"
key=".0:$.2" tabIndex={0}
onChange={[Function]} type="radio"
role="radio" />
tabIndex={0} <label
type="radio" className="throughputInputRadioBtnLabel"
/> htmlFor="Autoscale-input"
<label >
className="throughputInputRadioBtnLabel" Autoscale
htmlFor="Manual-input" </label>
key=".0:$.3" <input
> aria-label="Manual database throughput"
Manual aria-required={true}
</label> checked={false}
className="throughputInputRadioBtn"
id="Manual-input"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<label
className="throughputInputRadioBtnLabel"
htmlFor="Manual-input"
>
Manual
</label>
</div>
</div> </div>
</Stack> </Stack>
<Stack <Stack

View File

@@ -195,7 +195,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
</div> </div>
{node.children && ( {node.children && (
<AnimateHeight duration={TreeNodeComponent.transitionDurationMS} height={this.state.isExpanded ? "auto" : 0}> <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.getSortedChildren(node).map((childNode: TreeNode) => (
<TreeNodeComponent <TreeNodeComponent
key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`} key={`${childNode.label}-${generation + 1}-${childNode.timestamp}`}

View File

@@ -100,6 +100,7 @@ exports[`TreeNodeComponent does not render children by default 1`] = `
<div <div
className="nodeChildren" className="nodeChildren"
data-test="label" data-test="label"
role="group"
> >
<TreeNodeComponent <TreeNodeComponent
generation={3} generation={3}
@@ -251,6 +252,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
<div <div
className="nodeChildren" className="nodeChildren"
data-test="label" data-test="label"
role="group"
> >
<TreeNodeComponent <TreeNodeComponent
generation={13} generation={13}
@@ -352,6 +354,7 @@ exports[`TreeNodeComponent renders loading icon 1`] = `
<div <div
className="nodeChildren" className="nodeChildren"
data-test="label" data-test="label"
role="group"
/> />
</AnimateHeight> </AnimateHeight>
</div> </div>
@@ -465,6 +468,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
<div <div
className="nodeChildren" className="nodeChildren"
data-test="label" data-test="label"
role="group"
> >
<TreeNodeComponent <TreeNodeComponent
generation={13} generation={13}
@@ -594,6 +598,7 @@ exports[`TreeNodeComponent renders unsorted children by default 1`] = `
<div <div
className="nodeChildren" className="nodeChildren"
data-test="label" data-test="label"
role="group"
> >
<TreeNodeComponent <TreeNodeComponent
generation={3} generation={3}

View File

@@ -3,11 +3,11 @@
.treeComponent { .treeComponent {
.nodeItem { .nodeItem {
&:focus { &:focus {
outline: 1px dashed @AccentMedium; outline: 2px @AccentMedium;
} }
.treeNodeHeader { .treeNodeHeader {
padding: @SmallSpace 2px; margin: 1px;
cursor: pointer; cursor: pointer;
display: flex; display: flex;

View File

@@ -577,7 +577,7 @@ export default class Explorer {
try { try {
await Promise.all( await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => { databasesToLoad.map(async (database: ViewModels.Database) => {
await database.loadCollections(); await database.loadCollections(true);
const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id()); const isNewDatabase: boolean = _.some(newDatabases, (db: ViewModels.Database) => db.id() === database.id());
if (isNewDatabase) { if (isNewDatabase) {
database.expandDatabase(); database.expandDatabase();

View File

@@ -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 load more icon inlined as-is here: remove the style="fill:#374649;" so we can override it */}
<svg <svg
role="img" role="img"
aria-label="graph"
version="1.1" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink" xmlnsXlink="http://www.w3.org/1999/xlink"
@@ -135,6 +136,7 @@ export class GraphVizComponent extends React.Component<GraphVizComponentProps> {
<g id="triangleRight"> <g id="triangleRight">
<svg <svg
role="img" role="img"
aria-label="graph"
version="1.1" version="1.1"
id="Layer_1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -17,19 +17,21 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
<div className="middlePane"> <div className="middlePane">
<div className="graphTitle"> <div className="graphTitle">
<span className="paneTitle">Graph</span> <span className="paneTitle">Graph</span>
<span <button
style={{ border: "none", background: "none" }}
className="graphExpandCollapseBtn pull-right" className="graphExpandCollapseBtn pull-right"
onClick={this.props.toggleExpandGraph} onClick={this.props.toggleExpandGraph}
role="button" role="button"
aria-expanded={this.props.isTabsContentExpanded} aria-expanded={this.props.isTabsContentExpanded}
aria-name="View graph in full screen" aria-label={
tabIndex={0} this.props.isTabsContentExpanded ? "Collapse graph to minimized view" : "View graph in full screen"
}
> >
<img <img
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon} src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"} alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
/> />
</span> </button>
</div> </div>
<div className="maingraphContainer"> <div className="maingraphContainer">
<GraphVizComponent forceGraphParams={this.props.graphVizProps.forceGraphParams} /> <GraphVizComponent forceGraphParams={this.props.graphVizProps.forceGraphParams} />

View File

@@ -202,6 +202,7 @@ export function createControlCommandBarButtons(container: Explorer): CommandButt
if (showOpenFullScreen) { if (showOpenFullScreen) {
const label = "Open Full Screen"; const label = "Open Full Screen";
const fullScreenButton: CommandButtonComponentProps = { const fullScreenButton: CommandButtonComponentProps = {
id: "openFullScreenBtn",
iconSrc: OpenInTabIcon, iconSrc: OpenInTabIcon,
iconAlt: label, iconAlt: label,
onCommandClick: () => { onCommandClick: () => {

View File

@@ -147,12 +147,9 @@ export class NotificationConsoleComponent extends React.Component<
<div className="notificationConsoleControls"> <div className="notificationConsoleControls">
<Dropdown <Dropdown
label="Filter:" label="Filter:"
role="combobox"
selectedKey={this.state.selectedFilter} selectedKey={this.state.selectedFilter}
options={NotificationConsoleComponent.FilterOptions} options={NotificationConsoleComponent.FilterOptions}
onChange={this.onFilterSelected.bind(this)} onChange={this.onFilterSelected.bind(this)}
aria-labelledby="consoleFilterLabel"
aria-label={this.state.selectedFilter}
/> />
<span className="consoleSplitter" /> <span className="consoleSplitter" />
<span <span

View File

@@ -112,8 +112,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
className="notificationConsoleControls" className="notificationConsoleControls"
> >
<Dropdown <Dropdown
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:" label="Filter:"
onChange={[Function]} onChange={[Function]}
options={ options={
@@ -136,7 +134,6 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
}, },
] ]
} }
role="combobox"
selectedKey="All" selectedKey="All"
/> />
<span <span
@@ -278,8 +275,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
className="notificationConsoleControls" className="notificationConsoleControls"
> >
<Dropdown <Dropdown
aria-label="All"
aria-labelledby="consoleFilterLabel"
label="Filter:" label="Filter:"
onChange={[Function]} onChange={[Function]}
options={ options={
@@ -302,7 +297,6 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
}, },
] ]
} }
role="combobox"
selectedKey="All" selectedKey="All"
/> />
<span <span

View File

@@ -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);
});
});

View File

@@ -274,32 +274,34 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<input <div role="radiogroup">
className="panelRadioBtn" <input
checked={this.state.createNewDatabase} className="panelRadioBtn"
aria-label="Create new database" checked={this.state.createNewDatabase}
aria-checked={this.state.createNewDatabase} aria-label="Create new database"
name="databaseType" aria-checked={this.state.createNewDatabase}
type="radio" name="databaseType"
role="radio" type="radio"
id="databaseCreateNew" role="radio"
tabIndex={0} id="databaseCreateNew"
onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)} tabIndex={0}
/> onChange={this.onCreateNewDatabaseRadioBtnChange.bind(this)}
<span className="panelRadioBtnLabel">Create new</span> />
<span className="panelRadioBtnLabel">Create new</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.createNewDatabase} checked={!this.state.createNewDatabase}
aria-label="Use existing database" aria-label="Use existing database"
aria-checked={!this.state.createNewDatabase} aria-checked={!this.state.createNewDatabase}
name="databaseType" name="databaseType"
type="radio" type="radio"
role="radio" role="radio"
tabIndex={0} tabIndex={0}
onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)} onChange={this.onUseExistingDatabaseRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Use existing</span> <span className="panelRadioBtnLabel">Use existing</span>
</div>
</Stack> </Stack>
{this.state.createNewDatabase && ( {this.state.createNewDatabase && (
@@ -401,6 +403,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`} content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
> >
<Icon <Icon
role="button"
iconName="Info" iconName="Info"
className="panelInfoIcon" className="panelInfoIcon"
tabIndex={0} tabIndex={0}
@@ -802,35 +805,37 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
</Stack> </Stack>
<Stack horizontal verticalAlign="center"> <Stack horizontal verticalAlign="center">
<input <div role="radiogroup">
className="panelRadioBtn" <input
checked={this.state.enableAnalyticalStore} className="panelRadioBtn"
disabled={!this.isSynapseLinkEnabled()} checked={this.state.enableAnalyticalStore}
aria-label="Enable analytical store" disabled={!this.isSynapseLinkEnabled()}
aria-checked={this.state.enableAnalyticalStore} aria-label="Enable analytical store"
name="analyticalStore" aria-checked={this.state.enableAnalyticalStore}
type="radio" name="analyticalStore"
role="radio" type="radio"
id="enableAnalyticalStoreBtn" role="radio"
tabIndex={0} id="enableAnalyticalStoreBtn"
onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)} tabIndex={0}
/> onChange={this.onEnableAnalyticalStoreRadioBtnChange.bind(this)}
<span className="panelRadioBtnLabel">On</span> />
<span className="panelRadioBtnLabel">On</span>
<input <input
className="panelRadioBtn" className="panelRadioBtn"
checked={!this.state.enableAnalyticalStore} checked={!this.state.enableAnalyticalStore}
disabled={!this.isSynapseLinkEnabled()} disabled={!this.isSynapseLinkEnabled()}
aria-label="Disable analytical store" aria-label="Disable analytical store"
aria-checked={!this.state.enableAnalyticalStore} aria-checked={!this.state.enableAnalyticalStore}
name="analyticalStore" name="analyticalStore"
type="radio" type="radio"
role="radio" role="radio"
id="disableAnalyticalStoreBtn" id="disableAnalyticalStoreBtn"
tabIndex={0} tabIndex={0}
onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)} onChange={this.onDisableAnalyticalStoreRadioBtnChange.bind(this)}
/> />
<span className="panelRadioBtnLabel">Off</span> <span className="panelRadioBtnLabel">Off</span>
</div>
</Stack> </Stack>
{!this.isSynapseLinkEnabled() && ( {!this.isSynapseLinkEnabled() && (
@@ -979,7 +984,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
private getPartitionKeyPlaceHolder(index?: number): string { private getPartitionKeyPlaceHolder(index?: number): string {
switch (userContext.apiType) { switch (userContext.apiType) {
case "Mongo": case "Mongo":
return "e.g., address.zipCode"; return "e.g., categoryId";
case "Gremlin": case "Gremlin":
return "e.g., /address"; return "e.g., /address";
case "SQL": case "SQL":
@@ -1111,7 +1116,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
return userContext.apiType === "SQL" ? "/pk" : "pk"; return userContext.apiType === "SQL" ? "/pk" : "pk";
} }
if (this.props.isQuickstart) { if (this.props.isQuickstart) {
return userContext.apiType === "SQL" ? "/address" : "address"; return userContext.apiType === "SQL" ? "/categoryId" : "categoryId";
} }
return ""; return "";
} }

View File

@@ -48,7 +48,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
)} )}
</Text> </Text>
{showErrorDetails && ( {showErrorDetails && (
<a className="paneErrorLink" role="link" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}> <a className="paneErrorLink" role="button" onClick={expandConsole} tabIndex={0} onKeyPress={expandConsole}>
More details More details
</a> </a>
)} )}

View File

@@ -21,6 +21,11 @@ export const SettingsPane: FunctionComponent = () => {
const [customItemPerPage, setCustomItemPerPage] = useState<number>( const [customItemPerPage, setCustomItemPerPage] = useState<number>(
LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0 LocalStorageUtility.getEntryNumber(StorageKey.CustomItemPerPage) || 0
); );
const [containerPaginationEnabled, setContainerPaginationEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.ContainerPaginationEnabled)
? LocalStorageUtility.getEntryString(StorageKey.ContainerPaginationEnabled) === "true"
: false
);
const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>( const [crossPartitionQueryEnabled, setCrossPartitionQueryEnabled] = useState<boolean>(
LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled) LocalStorageUtility.hasItem(StorageKey.IsCrossPartitionQueryEnabled)
? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true" ? LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"
@@ -50,6 +55,7 @@ export const SettingsPane: FunctionComponent = () => {
isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage isCustomPageOptionSelected() ? customItemPerPage : Constants.Queries.unlimitedItemsPerPage
); );
LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage); LocalStorageUtility.setEntryNumber(StorageKey.CustomItemPerPage, customItemPerPage);
LocalStorageUtility.setEntryString(StorageKey.ContainerPaginationEnabled, containerPaginationEnabled.toString());
LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString()); LocalStorageUtility.setEntryString(StorageKey.IsCrossPartitionQueryEnabled, crossPartitionQueryEnabled.toString());
LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism); LocalStorageUtility.setEntryNumber(StorageKey.MaxDegreeOfParellism, maxDegreeOfParallelism);
@@ -185,6 +191,25 @@ export const SettingsPane: FunctionComponent = () => {
</div> </div>
</div> </div>
)} )}
<div className="settingsSection">
<div className="settingsSectionPart">
<div className="settingsSectionLabel">
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<Checkbox
styles={{
label: { padding: 0 },
}}
className="padding"
ariaLabel="Enable container pagination"
checked={containerPaginationEnabled}
onChange={() => setContainerPaginationEnabled(!containerPaginationEnabled)}
/>
</div>
</div>
{shouldShowCrossPartitionOption && ( {shouldShowCrossPartitionOption && (
<div className="settingsSection"> <div className="settingsSection">
<div className="settingsSectionPart"> <div className="settingsSectionPart">

View File

@@ -97,6 +97,35 @@ exports[`Settings Pane should render Default properly 1`] = `
</div> </div>
</div> </div>
</div> </div>
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
>
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable container pagination"
checked={false}
className="padding"
onChange={[Function]}
styles={
Object {
"label": Object {
"padding": 0,
},
}
}
/>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >
@@ -182,6 +211,35 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
<div <div
className="paneMainContent" className="paneMainContent"
> >
<div
className="settingsSection"
>
<div
className="settingsSectionPart"
>
<div
className="settingsSectionLabel"
>
Enable container pagination
<InfoTooltip>
Load 50 containers at a time. Currently, containers are not pulled in alphanumeric order.
</InfoTooltip>
</div>
<StyledCheckboxBase
ariaLabel="Enable container pagination"
checked={false}
className="padding"
onChange={[Function]}
styles={
Object {
"label": Object {
"padding": 0,
},
}
}
/>
</div>
</div>
<div <div
className="settingsSection" className="settingsSection"
> >

View File

@@ -42,39 +42,43 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
horizontal={true} horizontal={true}
verticalAlign="center" verticalAlign="center"
> >
<input <div
aria-checked={true} role="radiogroup"
aria-label="Create new database"
checked={true}
className="panelRadioBtn"
id="databaseCreateNew"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
> >
Create new <input
</span> aria-checked={true}
<input aria-label="Create new database"
aria-checked={false} checked={true}
aria-label="Use existing database" className="panelRadioBtn"
checked={false} id="databaseCreateNew"
className="panelRadioBtn" name="databaseType"
name="databaseType" onChange={[Function]}
onChange={[Function]} role="radio"
role="radio" tabIndex={0}
tabIndex={0} type="radio"
type="radio" />
/> <span
<span className="panelRadioBtnLabel"
className="panelRadioBtnLabel" >
> Create new
Use existing </span>
</span> <input
aria-checked={false}
aria-label="Use existing database"
checked={false}
className="panelRadioBtn"
name="databaseType"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Use existing
</span>
</div>
</Stack> </Stack>
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"
@@ -168,6 +172,7 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs." ariaLabel="Unique identifier for the container and used for id-based routing through REST and all SDKs."
className="panelInfoIcon" className="panelInfoIcon"
iconName="Info" iconName="Info"
role="button"
tabIndex={0} tabIndex={0}
/> />
</StyledTooltipHostBase> </StyledTooltipHostBase>
@@ -342,42 +347,46 @@ exports[`AddCollectionPanel should render Default properly 1`] = `
horizontal={true} horizontal={true}
verticalAlign="center" verticalAlign="center"
> >
<input <div
aria-checked={false} role="radiogroup"
aria-label="Enable analytical store"
checked={false}
className="panelRadioBtn"
disabled={true}
id="enableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
> >
On <input
</span> aria-checked={false}
<input aria-label="Enable analytical store"
aria-checked={true} checked={false}
aria-label="Disable analytical store" className="panelRadioBtn"
checked={true} disabled={true}
className="panelRadioBtn" id="enableAnalyticalStoreBtn"
disabled={true} name="analyticalStore"
id="disableAnalyticalStoreBtn" onChange={[Function]}
name="analyticalStore" role="radio"
onChange={[Function]} tabIndex={0}
role="radio" type="radio"
tabIndex={0} />
type="radio" <span
/> className="panelRadioBtnLabel"
<span >
className="panelRadioBtnLabel" On
> </span>
Off <input
</span> aria-checked={true}
aria-label="Disable analytical store"
checked={true}
className="panelRadioBtn"
disabled={true}
id="disableAnalyticalStoreBtn"
name="analyticalStore"
onChange={[Function]}
role="radio"
tabIndex={0}
type="radio"
/>
<span
className="panelRadioBtnLabel"
>
Off
</span>
</div>
</Stack> </Stack>
<Stack <Stack
className="panelGroupSpacing" className="panelGroupSpacing"

View File

@@ -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 { ReactTabKind, useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@@ -18,6 +18,11 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
return <></>; return <></>;
} }
let totalSteps = 9;
if (userContext.isTryCosmosDBSubscription) {
totalSteps = 10;
}
switch (step) { switch (step) {
case 1: case 1:
return isSampleDBExpanded ? ( return isSampleDBExpanded ? (
@@ -33,7 +38,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
}, },
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 1 of 8" footerContent={"Step 1 of " + totalSteps}
> >
Start viewing and working with your data by opening Documents under Data Start viewing and working with your data by opening Documents under Data
</TeachingBubble> </TeachingBubble>
@@ -55,7 +60,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(1), onClick: () => setStep(1),
}} }}
onDismiss={() => onDimissTeachingBubble()} 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 View documents here using the documents window. You can also use your favorite MongoDB tools and drivers to do
so. so.
@@ -78,7 +83,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(2), onClick: () => setStep(2),
}} }}
onDismiss={() => onDimissTeachingBubble()} 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 Add new document by copy / pasting JSON or uploading a JSON. You can also use your favorite MongoDB tools and
drivers to do so. drivers to do so.
@@ -99,7 +104,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(3), onClick: () => setStep(3),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 4 of 8" footerContent={"Step 4 of " + totalSteps}
> >
Query your data using the filter function. Azure Cosmos DB 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. MongoDB query language constructs. You can also use your favorite MongoDB tools and drivers to do so.
@@ -120,7 +125,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(4), onClick: () => setStep(4),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 5 of 8" footerContent={"Step 5 of " + totalSteps}
> >
Change throughput provisioned to your collection according to your needs Change throughput provisioned to your collection according to your needs
</TeachingBubble> </TeachingBubble>
@@ -140,7 +145,7 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(5), onClick: () => setStep(5),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 6 of 8" footerContent={"Step 6 of " + totalSteps}
> >
Use the indexing policy editor to create and edit your indexes. Use the indexing policy editor to create and edit your indexes.
</TeachingBubble> </TeachingBubble>
@@ -160,12 +165,54 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(6), onClick: () => setStep(6),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 7 of 8" footerContent={"Step 7 of " + totalSteps}
> >
Visualize your data, store queries in an interactive document Visualize your data, store queries in an interactive document
</TeachingBubble> </TeachingBubble>
); );
case 8: 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&apos;re ready, upgrade to production.
</TeachingBubble>
);
case 10:
return ( return (
<TeachingBubble <TeachingBubble
headline="Congratulations!" headline="Congratulations!"
@@ -180,10 +227,10 @@ export const MongoQuickstartTutorial: React.FC = (): JSX.Element => {
}} }}
secondaryButtonProps={{ secondaryButtonProps={{
text: "Previous", text: "Previous",
onClick: () => setStep(7), onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(9) : setStep(8)),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 8 of 8" footerContent={"Step " + totalSteps + " of " + totalSteps}
> >
<Stack> <Stack>
<Text style={{ color: "white" }}> <Text style={{ color: "white" }}>

View File

@@ -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 { ReactTabKind, useTabs } from "hooks/useTabs";
import { useTeachingBubble } from "hooks/useTeachingBubble"; import { useTeachingBubble } from "hooks/useTeachingBubble";
import React from "react"; import React from "react";
@@ -17,6 +17,10 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
if (userContext.apiType !== "SQL") { if (userContext.apiType !== "SQL") {
return <></>; return <></>;
} }
let totalSteps = 8;
if (userContext.isTryCosmosDBSubscription) {
totalSteps = 9;
}
switch (step) { switch (step) {
case 1: case 1:
@@ -33,7 +37,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
}, },
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 1 of 7" footerContent={"Step 1 of " + totalSteps}
> >
Start viewing and working with your data by opening Items under Data Start viewing and working with your data by opening Items under Data
</TeachingBubble> </TeachingBubble>
@@ -55,7 +59,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(1), onClick: () => setStep(1),
}} }}
onDismiss={() => onDimissTeachingBubble()} 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 View item here using the items window. Additionally you can also filter items to be reviewed with the filter
function function
@@ -78,7 +82,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(2), onClick: () => setStep(2),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 3 of 7" footerContent={"Step 3 of " + totalSteps}
> >
Add new item by copy / pasting JSON; or uploading a JSON Add new item by copy / pasting JSON; or uploading a JSON
</TeachingBubble> </TeachingBubble>
@@ -98,7 +102,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(3), onClick: () => setStep(3),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 4 of 7" footerContent={"Step 4 of " + totalSteps}
> >
Query your data using either the filter function or new query. Query your data using either the filter function or new query.
</TeachingBubble> </TeachingBubble>
@@ -118,7 +122,7 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(4), onClick: () => setStep(4),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 5 of 7" footerContent={"Step 5 of " + totalSteps}
> >
Change throughput provisioned to your container according to your needs Change throughput provisioned to your container according to your needs
</TeachingBubble> </TeachingBubble>
@@ -138,12 +142,54 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
onClick: () => setStep(5), onClick: () => setStep(5),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 6 of 7" footerContent={"Step 6 of " + totalSteps}
> >
Visualize your data, store queries in an interactive document Visualize your data, store queries in an interactive document
</TeachingBubble> </TeachingBubble>
); );
case 7: 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&apos;re ready, upgrade to production.
</TeachingBubble>
);
case 9:
return ( return (
<TeachingBubble <TeachingBubble
headline="Congratulations!" headline="Congratulations!"
@@ -158,10 +204,10 @@ export const SQLQuickstartTutorial: React.FC = (): JSX.Element => {
}} }}
secondaryButtonProps={{ secondaryButtonProps={{
text: "Previous", text: "Previous",
onClick: () => setStep(6), onClick: () => (userContext.isTryCosmosDBSubscription ? setStep(8) : setStep(7)),
}} }}
onDismiss={() => onDimissTeachingBubble()} onDismiss={() => onDimissTeachingBubble()}
footerContent="Step 7 of 7" footerContent={"Step " + totalSteps + " of " + totalSteps}
> >
<Stack> <Stack>
<Text style={{ color: "white" }}> <Text style={{ color: "white" }}>

View File

@@ -9,7 +9,7 @@ import {
Stack, Stack,
TeachingBubble, TeachingBubble,
TeachingBubbleContent, TeachingBubbleContent,
Text Text,
} from "@fluentui/react"; } from "@fluentui/react";
import { sendMessage } from "Common/MessageHandler"; import { sendMessage } from "Common/MessageHandler";
import { MessageTypes } from "Contracts/ExplorerContracts"; import { MessageTypes } from "Contracts/ExplorerContracts";
@@ -116,7 +116,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
<form className="connectExplorerFormContainer"> <form className="connectExplorerFormContainer">
<div className="splashScreenContainer"> <div className="splashScreenContainer">
<div className="splashScreen"> <div className="splashScreen">
<div className="title"> <div
className="title"
aria-label={
userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"
}
>
{userContext.apiType === "Postgres" {userContext.apiType === "Postgres"
? "Welcome to Azure Cosmos DB for PostgreSQL" ? "Welcome to Azure Cosmos DB for PostgreSQL"
: "Welcome to Azure Cosmos DB"} : "Welcome to Azure Cosmos DB"}
@@ -563,7 +570,17 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} }
private getLearningResourceItems(): JSX.Element { 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) { switch (userContext.apiType) {
case "SQL": case "SQL":
case "Postgres": case "Postgres":
@@ -573,11 +590,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Get Started using an SDK", title: "Get Started using an SDK",
description: "Learn about the Azure Cosmos DB SDK.", description: "Learn about the Azure Cosmos DB SDK.",
}, },
{ cdbLiveTv,
link: "https://aka.ms/msl-complex-queries",
title: "Master Complex Queries",
description: "Learn how to author complex queries.",
},
{ {
link: "https://aka.ms/msl-move-data", link: "https://aka.ms/msl-move-data",
title: "Migrate Your Data", title: "Migrate Your Data",
@@ -597,11 +610,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Getting Started Guide", title: "Getting Started Guide",
description: "Learn the basics to get started.", description: "Learn the basics to get started.",
}, },
{ cdbLiveTv,
link: "http://aka.ms/mongodotnet",
title: "Build a web API",
description: "Create a web API with the.NET SDK.",
},
]; ];
break; break;
case "Cassandra": case "Cassandra":
@@ -611,11 +620,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Create a Container", title: "Create a Container",
description: "Get to know the create a container options.", description: "Get to know the create a container options.",
}, },
{ cdbLiveTv,
link: "https://aka.ms/cassandraserverdiagnostics",
title: "Run Server Diagnostics",
description: "Learn how to run server diagnostics.",
},
{ {
link: "https://aka.ms/Cassandrathroughput", link: "https://aka.ms/Cassandrathroughput",
title: "Provision Throughput", title: "Provision Throughput",
@@ -635,11 +640,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Import Graph Data", title: "Import Graph Data",
description: "Learn Bulk ingestion data using BulkExecutor", description: "Learn Bulk ingestion data using BulkExecutor",
}, },
{ cdbLiveTv,
link: "https://aka.ms/graphoptimize",
title: "Optimize your Queries",
description: "Learn how to evaluate your Gremlin queries",
},
]; ];
break; break;
case "Tables": case "Tables":
@@ -654,11 +655,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
title: "Build a Java App", title: "Build a Java App",
description: "Create a Azure Cosmos DB for Table app with Java SDK ", description: "Create a Azure Cosmos DB for Table app with Java SDK ",
}, },
{ cdbLiveTv,
link: "https://aka.ms/tablenodejs",
title: "Build a Node.js App",
description: "Create a Azure Cosmos DB for Table app with Node.js SDK",
},
]; ];
break; break;
} }

View File

@@ -77,7 +77,14 @@
<div class="addClause" data-bind=" "> <div class="addClause" data-bind=" ">
<div class="addClause-heading"> <div class="addClause-heading">
<span class="clause-table addClause-title"> <span class="clause-table addClause-title">
<img class="addclauseProperty-Img" style="margin-bottom: 5px" src="/Add-property.svg" /> <img
class="addclauseProperty-Img"
style="margin-bottom: 5px"
src="/Add-property.svg"
alt="Add new clause"
aria-hidden="true"
role="none"
/>
<span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span> <span style="margin-left: 5px" data-bind="text: addNewClauseLine"></span>
</span> </span>
</div> </div>

View File

@@ -92,6 +92,7 @@ function TabNav({ tab, active, tabKind }: { tab?: Tab; active: boolean; tabKind?
} }
}} }}
className={active ? "active tabList" : "tabList"} className={active ? "active tabList" : "tabList"}
style={active ? { fontWeight: "bolder" } : {}}
title={useObservable(tab?.tabPath || ko.observable(""))} title={useObservable(tab?.tabPath || ko.observable(""))}
aria-selected={active} aria-selected={active}
aria-expanded={active} aria-expanded={active}

View File

@@ -3,7 +3,7 @@ import React from "react";
import * as _ from "underscore"; import * as _ from "underscore";
import { AuthType } from "../../AuthType"; import { AuthType } from "../../AuthType";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import { readCollections } from "../../Common/dataAccess/readCollections"; import { readCollections, readCollectionsWithPagination } from "../../Common/dataAccess/readCollections";
import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer"; import { readDatabaseOffer } from "../../Common/dataAccess/readDatabaseOffer";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import * as Logger from "../../Common/Logger"; import * as Logger from "../../Common/Logger";
@@ -13,6 +13,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import { useSidePanel } from "../../hooks/useSidePanel"; import { useSidePanel } from "../../hooks/useSidePanel";
import { useTabs } from "../../hooks/useTabs"; import { useTabs } from "../../hooks/useTabs";
import { IJunoResponse, JunoClient } from "../../Juno/JunoClient"; import { IJunoResponse, JunoClient } from "../../Juno/JunoClient";
import * as StorageUtility from "../../Shared/StorageUtility";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
@@ -38,6 +39,7 @@ export default class Database implements ViewModels.Database {
public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>; public selectedSubnodeKind: ko.Observable<ViewModels.CollectionTabKind>;
public junoClient: JunoClient; public junoClient: JunoClient;
public isSampleDB: boolean; public isSampleDB: boolean;
public collectionsContinuationToken?: string;
private isOfferRead: boolean; private isOfferRead: boolean;
constructor(container: Explorer, data: DataModels.Database) { constructor(container: Explorer, data: DataModels.Database) {
@@ -140,7 +142,11 @@ export default class Database implements ViewModels.Database {
} }
await this.loadOffer(); await this.loadOffer();
await this.loadCollections();
if (this.collections()?.length === 0) {
await this.loadCollections(true);
}
this.isDatabaseExpanded(true); this.isDatabaseExpanded(true);
TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ExpandTreeNode, ActionModifiers.Mark, {
description: "Database node", description: "Database node",
@@ -162,9 +168,31 @@ export default class Database implements ViewModels.Database {
}); });
} }
public async loadCollections(): Promise<void> { public async loadCollections(restart = false) {
const collectionVMs: Collection[] = []; const collectionVMs: Collection[] = [];
const collections: DataModels.Collection[] = await readCollections(this.id()); let collections: DataModels.Collection[] = [];
if (restart) {
this.collectionsContinuationToken = undefined;
}
const containerPaginationEnabled =
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) ===
"true";
if (containerPaginationEnabled) {
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
this.id(),
this.collectionsContinuationToken
);
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
this.collectionsContinuationToken = collectionsWithPagination.continuationToken;
} else {
this.collectionsContinuationToken = undefined;
}
collections = collectionsWithPagination.collections;
} else {
collections = await readCollections(this.id());
}
// TODO Remove // TODO Remove
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property // This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) { if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
@@ -199,7 +227,9 @@ export default class Database implements ViewModels.Database {
//merge collections //merge collections
this.addCollectionsToList(collectionVMs); this.addCollectionsToList(collectionVMs);
this.deleteCollectionsFromList(deltaCollections.toDelete); if (!containerPaginationEnabled || restart) {
this.deleteCollectionsFromList(deltaCollections.toDelete);
}
useDatabases.getState().updateDatabase(this); useDatabases.getState().updateDatabase(this);
} }

View File

@@ -479,6 +479,18 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
databaseNode.children.push(buildCollectionNode(database, collection)) databaseNode.children.push(buildCollectionNode(database, collection))
); );
if (database.collectionsContinuationToken) {
const loadMoreNode: TreeNode = {
label: "load more",
className: "loadMoreHeader",
onClick: async () => {
await database.loadCollections();
useDatabases.getState().updateDatabase(database);
},
};
databaseNode.children.push(loadMoreNode);
}
database.collections.subscribe((collections: ViewModels.Collection[]) => { database.collections.subscribe((collections: ViewModels.Collection[]) => {
collections.forEach((collection: ViewModels.Collection) => collections.forEach((collection: ViewModels.Collection) =>
databaseNode.children.push(buildCollectionNode(database, collection)) databaseNode.children.push(buildCollectionNode(database, collection))

View File

@@ -80,6 +80,7 @@ const App: React.FunctionComponent = () => {
return ( return (
<div className="flexContainer"> <div className="flexContainer">
<div id="divExplorer" className="flexContainer hideOverflows"> <div id="divExplorer" className="flexContainer hideOverflows">
<div id="freeTierTeachingBubble"> </div>
{/* Main Command Bar - Start */} {/* Main Command Bar - Start */}
<CommandBar container={explorer} /> <CommandBar container={explorer} />
{/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - Begin */}

View File

@@ -4,6 +4,7 @@ import * as SessionStorageUtility from "./SessionStorageUtility";
export { LocalStorageUtility, SessionStorageUtility }; export { LocalStorageUtility, SessionStorageUtility };
export enum StorageKey { export enum StorageKey {
ActualItemPerPage, ActualItemPerPage,
ContainerPaginationEnabled,
CustomItemPerPage, CustomItemPerPage,
DatabaseAccountId, DatabaseAccountId,
EncryptedKeyToken, EncryptedKeyToken,

View File

@@ -49,7 +49,7 @@ export function getMsalInstance() {
cacheLocation: "localStorage", cacheLocation: "localStorage",
}, },
auth: { auth: {
authority: `${configContext.AAD_ENDPOINT}common`, authority: `${configContext.AAD_ENDPOINT}organizations`,
clientId: "203f1145-856a-4232-83d4-a43568fba23d", clientId: "203f1145-856a-4232-83d4-a43568fba23d",
}, },
}; };

View File

@@ -21,6 +21,23 @@ function isValidOrigin(allowedOrigins: ReadonlyArray<string>, event: MessageEven
return false; 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 { export function isReadyMessage(event: MessageEvent): boolean {
if (!event?.data?.kind && !event?.data?.data) { if (!event?.data?.kind && !event?.data?.data) {
return false; return false;

View File

@@ -10,7 +10,7 @@ const PortalIPs: { [key: string]: string[] } = {
usnat: ["7.28.202.68"], usnat: ["7.28.202.68"],
}; };
export const getNetworkSettingsWarningMessage = (clientIpAddress: string): string => { export const getNetworkSettingsWarningMessage = (): string => {
const accountProperties = userContext.databaseAccount?.properties; const accountProperties = userContext.databaseAccount?.properties;
if (!accountProperties) { if (!accountProperties) {
@@ -40,13 +40,7 @@ export const getNetworkSettingsWarningMessage = (clientIpAddress: string): strin
if (numberOfMatches !== portalIPs.length) { 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 "The Network settings for this account are preventing access from Data Explorer. Please allow access from Azure Portal to proceed.";
} }
return "";
} else {
if (!clientIpAddress || ipRules.some((ipRule) => ipRule.ipAddressOrRange === clientIpAddress)) {
return "";
}
return "The Network settings for this account are preventing access from Data Explorer. Please add your current IP to the firewall rules to proceed.";
} }
return "";
}; };

View File

@@ -1,3 +1,4 @@
import Explorer from "Explorer/Explorer";
import { ReactTabKind, useTabs } from "hooks/useTabs"; import { ReactTabKind, useTabs } from "hooks/useTabs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility"; import { getNetworkSettingsWarningMessage } from "Utils/NetworkUtility";
@@ -10,7 +11,6 @@ import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts"; import { ActionType, DataExplorerAction } from "../Contracts/ActionContracts";
import { MessageTypes } from "../Contracts/ExplorerContracts"; import { MessageTypes } from "../Contracts/ExplorerContracts";
import { DataExplorerInputsFrame } from "../Contracts/ViewModels"; import { DataExplorerInputsFrame } from "../Contracts/ViewModels";
import Explorer from "../Explorer/Explorer";
import { handleOpenAction } from "../Explorer/OpenActions/OpenActions"; import { handleOpenAction } from "../Explorer/OpenActions/OpenActions";
import { useDatabases } from "../Explorer/useDatabases"; import { useDatabases } from "../Explorer/useDatabases";
import { import {
@@ -33,7 +33,7 @@ import { Node, PortalEnv, updateUserContext, userContext } from "../UserContext"
import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types"; import { DatabaseAccountListKeysResult } from "../Utils/arm/generatedClients/cosmos/types";
import { getMsalInstance } from "../Utils/AuthorizationUtils"; 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 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 // This hook has a LOT of magic, but ideally we can delete it once we have removed KO and switched entirely to React
@@ -239,6 +239,7 @@ async function configurePortal(): Promise<Explorer> {
updateUserContext({ updateUserContext({
authType: AuthType.AAD, authType: AuthType.AAD,
}); });
let explorer: Explorer;
return new Promise((resolve) => { return new Promise((resolve) => {
// In development mode, try to load the iframe message from session storage. // In development mode, try to load the iframe message from session storage.
// This allows webpack hot reload to function properly in the portal // This allows webpack hot reload to function properly in the portal
@@ -251,7 +252,7 @@ async function configurePortal(): Promise<Explorer> {
); );
console.dir(message); console.dir(message);
updateContextsFromPortalMessage(message); updateContextsFromPortalMessage(message);
const explorer = new Explorer(); explorer = new Explorer();
// In development mode, save the iframe message from the portal in session storage. // In development mode, save the iframe message from the portal in session storage.
// This allows webpack hot reload to funciton properly // This allows webpack hot reload to funciton properly
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
@@ -287,7 +288,7 @@ async function configurePortal(): Promise<Explorer> {
} }
updateContextsFromPortalMessage(inputs); updateContextsFromPortalMessage(inputs);
const explorer = new Explorer(); explorer = new Explorer();
resolve(explorer); resolve(explorer);
if (openAction) { if (openAction) {
handleOpenAction(openAction, useDatabases.getState().databases, explorer); handleOpenAction(openAction, useDatabases.getState().databases, explorer);
@@ -300,6 +301,8 @@ async function configurePortal(): Promise<Explorer> {
} else { } else {
useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId); useTabs.getState().closeTabsByComparator((tab) => tab.tabId === event.data?.data?.tabId);
} }
} else if (message?.type === MessageTypes.RefreshResources) {
explorer.onRefreshResourcesClick();
} }
}, },
false false
@@ -314,23 +317,6 @@ function shouldForwardMessage(message: PortalMessage, messageOrigin: string) {
return messageOrigin === window.document.location.origin && message.type === MessageTypes.TelemetryInfo; 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) { function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
if ( if (
configContext.BACKEND_ENDPOINT && configContext.BACKEND_ENDPOINT &&
@@ -382,7 +368,7 @@ function updateContextsFromPortalMessage(inputs: DataExplorerInputsFrame) {
} }
} }
const warningMessage = getNetworkSettingsWarningMessage(inputs.clientIpAddress); const warningMessage = getNetworkSettingsWarningMessage();
useTabs.getState().setNetworkSettingsWarning(warningMessage); useTabs.getState().setNetworkSettingsWarning(warningMessage);
if (inputs.features) { if (inputs.features) {