Merge branch 'master' into languy-resource-tree-to-react

This commit is contained in:
Laurent Nguyen 2021-03-12 11:21:01 +01:00
commit 21b92ed4f8
101 changed files with 28560 additions and 1839 deletions

View File

@ -11,15 +11,9 @@ src/Common/CosmosClient.test.ts
src/Common/CosmosClient.ts src/Common/CosmosClient.ts
src/Common/DataAccessUtilityBase.test.ts src/Common/DataAccessUtilityBase.test.ts
src/Common/DataAccessUtilityBase.ts src/Common/DataAccessUtilityBase.ts
src/Common/DeleteFeedback.ts
src/Common/DocumentClientUtilityBase.ts
src/Common/EditableUtility.ts src/Common/EditableUtility.ts
src/Common/HashMap.test.ts src/Common/HashMap.test.ts
src/Common/HashMap.ts src/Common/HashMap.ts
src/Common/HeadersUtility.test.ts
src/Common/HeadersUtility.ts
src/Common/IteratorUtilities.test.ts
src/Common/IteratorUtilities.ts
src/Common/Logger.test.ts src/Common/Logger.test.ts
src/Common/MessageHandler.test.ts src/Common/MessageHandler.test.ts
src/Common/MessageHandler.ts src/Common/MessageHandler.ts
@ -30,7 +24,6 @@ src/Common/ObjectCache.test.ts
src/Common/ObjectCache.ts src/Common/ObjectCache.ts
src/Common/QueriesClient.ts src/Common/QueriesClient.ts
src/Common/Splitter.ts src/Common/Splitter.ts
src/Common/ThemeUtility.ts
src/Common/UrlUtility.ts src/Common/UrlUtility.ts
src/Config.ts src/Config.ts
src/Contracts/ActionContracts.ts src/Contracts/ActionContracts.ts
@ -58,8 +51,6 @@ src/Explorer/ComponentRegisterer.test.ts
src/Explorer/ComponentRegisterer.ts src/Explorer/ComponentRegisterer.ts
src/Explorer/ContextMenuButtonFactory.ts src/Explorer/ContextMenuButtonFactory.ts
src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts src/Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.ts
src/Explorer/Controls/CommandButton/CommandButton.test.ts
src/Explorer/Controls/CommandButton/CommandButton.ts
src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts src/Explorer/Controls/DiffEditor/DiffEditorComponent.ts
src/Explorer/Controls/DynamicList/DynamicList.test.ts src/Explorer/Controls/DynamicList/DynamicList.test.ts
src/Explorer/Controls/DynamicList/DynamicListComponent.ts src/Explorer/Controls/DynamicList/DynamicListComponent.ts
@ -95,8 +86,6 @@ src/Explorer/Graph/GraphExplorerComponent/D3ForceGraph.ts
src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts src/Explorer/Graph/GraphExplorerComponent/EdgeInfoCache.ts
src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts src/Explorer/Graph/GraphExplorerComponent/GraphData.test.ts
src/Explorer/Graph/GraphExplorerComponent/GraphData.ts src/Explorer/Graph/GraphExplorerComponent/GraphData.ts
src/Explorer/Graph/GraphExplorerComponent/GraphUtil.test.ts
src/Explorer/Graph/GraphExplorerComponent/GraphUtil.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.test.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts src/Explorer/Graph/GraphExplorerComponent/GremlinClient.ts
src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.test.ts
@ -110,7 +99,6 @@ src/Explorer/Menus/CommandBar/CommandBarComponentButtonFactory.ts
src/Explorer/Menus/ContextMenu.ts src/Explorer/Menus/ContextMenu.ts
src/Explorer/MostRecentActivity/MostRecentActivity.ts src/Explorer/MostRecentActivity/MostRecentActivity.ts
src/Explorer/Notebook/FileSystemUtil.ts src/Explorer/Notebook/FileSystemUtil.ts
src/Explorer/Notebook/NTeractUtil.ts
src/Explorer/Notebook/NotebookClientV2.ts src/Explorer/Notebook/NotebookClientV2.ts
src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts src/Explorer/Notebook/NotebookComponent/NotebookContentProvider.ts
src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts src/Explorer/Notebook/NotebookComponent/__mocks__/rx-jupyter.ts
@ -170,7 +158,6 @@ src/Explorer/Tables/DataTable/DataTableBuilder.ts
src/Explorer/Tables/DataTable/DataTableContextMenu.ts src/Explorer/Tables/DataTable/DataTableContextMenu.ts
src/Explorer/Tables/DataTable/DataTableOperationManager.ts src/Explorer/Tables/DataTable/DataTableOperationManager.ts
src/Explorer/Tables/DataTable/DataTableOperations.ts src/Explorer/Tables/DataTable/DataTableOperations.ts
src/Explorer/Tables/DataTable/DataTableUtilities.ts
src/Explorer/Tables/DataTable/DataTableViewModel.ts src/Explorer/Tables/DataTable/DataTableViewModel.ts
src/Explorer/Tables/DataTable/TableCommands.ts src/Explorer/Tables/DataTable/TableCommands.ts
src/Explorer/Tables/DataTable/TableEntityCache.ts src/Explorer/Tables/DataTable/TableEntityCache.ts
@ -179,8 +166,6 @@ src/Explorer/Tables/Entities.ts
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts
src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
src/Explorer/Tables/QueryBuilder/QueryViewModel.ts src/Explorer/Tables/QueryBuilder/QueryViewModel.ts
@ -263,8 +248,6 @@ src/Shared/ExplorerSettings.ts
src/Shared/PriceEstimateCalculator.ts src/Shared/PriceEstimateCalculator.ts
src/Shared/StorageUtility.test.ts src/Shared/StorageUtility.test.ts
src/Shared/StorageUtility.ts src/Shared/StorageUtility.ts
src/Shared/StringUtility.test.ts
src/Shared/StringUtility.ts
src/Shared/appInsights.ts src/Shared/appInsights.ts
src/SparkClusterManager/ArcadiaResourceManager.ts src/SparkClusterManager/ArcadiaResourceManager.ts
src/SparkClusterManager/SparkClusterManager.ts src/SparkClusterManager/SparkClusterManager.ts
@ -273,25 +256,14 @@ src/Terminal/NotebookAppContracts.d.ts
src/Terminal/index.ts src/Terminal/index.ts
src/TokenProviders/PortalTokenProvider.ts src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts src/TokenProviders/TokenProviderFactory.ts
src/Utils/AuthorizationUtils.test.ts
src/Utils/AuthorizationUtils.ts
src/Utils/AutoPilotUtils.test.ts
src/Utils/AutoPilotUtils.ts
src/Utils/DatabaseAccountUtils.test.ts src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts src/Utils/DatabaseAccountUtils.ts
src/Utils/JunoUtils.ts
src/Utils/MessageValidation.ts
src/Utils/NotebookConfigurationUtils.ts
src/Utils/PricingUtils.test.ts src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts src/Utils/QueryUtils.ts
src/Utils/StringUtils.test.ts
src/Utils/StringUtils.ts
src/applyExplorerBindings.ts src/applyExplorerBindings.ts
src/global.d.ts src/global.d.ts
src/quickstart.ts
src/setupTests.ts src/setupTests.ts
src/workers/upload/definitions.ts
src/workers/upload/index.ts src/workers/upload/index.ts
src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx src/Explorer/Controls/AccessibleElement/AccessibleElement.tsx
src/Explorer/Controls/Accordion/AccordionComponent.tsx src/Explorer/Controls/Accordion/AccordionComponent.tsx
@ -338,15 +310,7 @@ src/Explorer/Graph/GraphExplorerComponent/QueryContainerComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNeighborsComponent.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.tsx
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
src/Explorer/Menus/CommandBar/CommandBarComponentAdapter.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.test.tsx
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
src/Explorer/Menus/CommandBar/MemoryTrackerComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponent.tsx
src/Explorer/Menus/NavBar/ControlBarComponentAdapter.tsx
src/Explorer/Menus/NavBar/MeControlComponent.test.tsx
src/Explorer/Menus/NavBar/MeControlComponent.tsx
src/Explorer/Menus/NavBar/MeControlComponentAdapter.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.test.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponent.tsx
src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx src/Explorer/Menus/NotificationConsole/NotificationConsoleComponentAdapter.tsx

View File

@ -94,7 +94,7 @@ jobs:
path: dist/ path: dist/
endtoendemulator: endtoendemulator:
name: "End To End Emulator Tests" name: "End To End Emulator Tests"
needs: [lint, format, compile, unittest] if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -143,23 +143,9 @@ jobs:
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
endtoendhosted: endtoendhosted:
name: "End to End Hosted Tests" name: "End to End Tests"
needs: [lint, format, compile, unittest] needs: [cleanupaccounts]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: End to End Hosted Tests
run: |
npm ci
npm start &
node utils/cleanupDBs.js
npm run wait-for-server
npm run test:e2e
shell: bash
env: env:
NODE_TLS_REJECT_UNAUTHORIZED: 0 NODE_TLS_REJECT_UNAUTHORIZED: 0
PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }} PORTAL_RUNNER_SUBSCRIPTION: ${{ secrets.PORTAL_RUNNER_SUBSCRIPTION }}
@ -176,15 +162,52 @@ jobs:
CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }} CASSANDRA_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_CASSANDRA }}
TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }} TABLES_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_TABLE }}
DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html" DATA_EXPLORER_ENDPOINT: "https://localhost:1234/hostedExplorer.html"
strategy:
matrix:
test-file:
- ./test/cassandra/container.spec.ts
- ./test/mongo/mongoIndexPolicy.spec.ts
- ./test/notebooks/uploadAndOpenNotebook.spec.ts
- ./test/selfServe/selfServeExample.spec.ts
- ./test/sql/container.spec.ts
- ./test/sql/resourceToken.spec.ts
- ./test/tables/container.spec.ts
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: npm start &
- run: node utils/cleanupDBs.js
- run: npm run wait-for-server
- name: ${{ matrix['test-file'] }}
run: npx jest -c ./jest.config.e2e.js --detectOpenHandles ${{ matrix['test-file'] }}
shell: bash
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
if: failure() if: failure()
with: with:
name: screenshots name: screenshots
path: failed-* path: failed-*
cleanupaccounts:
name: "Cleanup Test Database Accounts"
runs-on: ubuntu-latest
env:
NOTEBOOKS_TEST_RUNNER_CLIENT_ID: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_ID }}
NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET: ${{ secrets.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js 14.x
uses: actions/setup-node@v1
with:
node-version: 14.x
- run: npm ci
- run: node utils/cleanupDBs.js
nuget: nuget:
name: Publish Nuget name: Publish Nuget
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@ -200,7 +223,7 @@ jobs:
- run: cp ./configs/prod.json config.json - 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 "GitHub" -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 -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
name: packages name: packages
with: with:
@ -208,7 +231,7 @@ jobs:
nugetmpac: nugetmpac:
name: Publish Nuget MPAC name: Publish Nuget MPAC
if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/') if: github.ref == 'refs/heads/master' || contains(github.ref, 'hotfix/') || contains(github.ref, 'release/')
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendhosted, accessibility] needs: [build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }} NUGET_SOURCE: ${{ secrets.NUGET_SOURCE }}
@ -225,7 +248,7 @@ jobs:
- 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 "GitHub" -Password "$AZURE_DEVOPS_PAT" - run: nuget sources add -Name "ADO" -Source "$NUGET_SOURCE" -UserName "GitHub" -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 -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
name: packages name: packages
with: with:

View File

@ -17,5 +17,10 @@
"test/out/**": true, "test/out/**": true,
"workers/libs/**": true "workers/libs/**": true
}, },
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
}
} }

26435
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
"@azure/cosmos": "3.9.0", "@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5", "@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.2.1", "@azure/identity": "1.2.1",
"@azure/ms-rest-nodeauth": "3.0.7",
"@babel/plugin-proposal-class-properties": "7.12.1", "@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12", "@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.2", "@jupyterlab/services": "6.0.2",
@ -76,6 +77,7 @@
"knockout": "3.5.1", "knockout": "3.5.1",
"mkdirp": "1.0.4", "mkdirp": "1.0.4",
"monaco-editor": "0.18.1", "monaco-editor": "0.18.1",
"ms": "2.1.3",
"msal": "1.4.4", "msal": "1.4.4",
"object.entries": "1.1.0", "object.entries": "1.1.0",
"office-ui-fabric-react": "7.134.1", "office-ui-fabric-react": "7.134.1",

View File

@ -1,28 +1,5 @@
import * as Constants from "./Constants";
import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../Shared/StorageUtility";
// x-ms-resource-quota: databases = 100; collections = 5000; users = 500000; permissions = 2000000;
export function getQuota(responseHeaders: any): any {
return responseHeaders && responseHeaders[Constants.HttpHeaders.resourceQuota]
? parseStringIntoObject(responseHeaders[Constants.HttpHeaders.resourceQuota])
: null;
}
export function shouldEnableCrossPartitionKey(): boolean { export function shouldEnableCrossPartitionKey(): boolean {
return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true"; return LocalStorageUtility.getEntryString(StorageKey.IsCrossPartitionQueryEnabled) === "true";
} }
function parseStringIntoObject(resourceString: string) {
var entityObject: any = {};
if (resourceString) {
var entitiesArray: string[] = resourceString.split(";");
for (var i: any = 0; i < entitiesArray.length; i++) {
var entity: string[] = entitiesArray[i].split("=");
entityObject[entity[0]] = entity[1];
}
}
return entityObject;
}

View File

@ -1,6 +1,8 @@
import { QueryResults } from "../Contracts/ViewModels"; import { QueryResults } from "../Contracts/ViewModels";
interface QueryResponse { interface QueryResponse {
// [Todo] remove any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resources: any[]; resources: any[];
hasMoreResults: boolean; hasMoreResults: boolean;
activityId: string; activityId: string;
@ -16,6 +18,7 @@ export interface MinimalQueryIterator {
export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> { export function nextPage(documentsIterator: MinimalQueryIterator, firstItemIndex: number): Promise<QueryResults> {
return documentsIterator.fetchNext().then((response) => { return documentsIterator.fetchNext().then((response) => {
const documents = response.resources; const documents = response.resources;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const headers = (response as any).headers || {}; // TODO this is a private key. Remove any const headers = (response as any).headers || {}; // TODO this is a private key. Remove any
const itemCount = (documents && documents.length) || 0; const itemCount = (documents && documents.length) || 0;
return { return {

View File

@ -2,8 +2,7 @@
* Copyright (C) Microsoft Corporation. All rights reserved. * Copyright (C) Microsoft Corporation. All rights reserved.
*----------------------------------------------------------*/ *----------------------------------------------------------*/
export default class ThemeUtility { export function getMonacoTheme(theme: string): string {
public static getMonacoTheme(theme: string): string {
switch (theme) { switch (theme) {
case "default": case "default":
case "hc-white": case "hc-white":
@ -16,4 +15,3 @@ export default class ThemeUtility {
return "vs"; return "vs";
} }
} }
}

View File

@ -9,10 +9,10 @@ export interface DatabaseAccount {
} }
export interface DatabaseAccountExtendedProperties { export interface DatabaseAccountExtendedProperties {
documentEndpoint: string; documentEndpoint?: string;
tableEndpoint: string; tableEndpoint?: string;
gremlinEndpoint: string; gremlinEndpoint?: string;
cassandraEndpoint: string; cassandraEndpoint?: string;
configurationOverrides?: ConfigurationOverrides; configurationOverrides?: ConfigurationOverrides;
capabilities?: Capability[]; capabilities?: Capability[];
enableMultipleWriteLocations?: boolean; enableMultipleWriteLocations?: boolean;

View File

@ -0,0 +1,9 @@
/**
* Messaging types used with SelfServe Component <-> Portal communication
* and Hosted <-> SelfServe Component communication
*/
export enum SelfServeMessageTypes {
TelemetryInfo = "TelemetryInfo",
Notification = "Notification",
}

View File

@ -393,7 +393,16 @@ export interface DataExplorerInputsFrame {
isAuthWithresourceToken?: boolean; isAuthWithresourceToken?: boolean;
defaultCollectionThroughput?: CollectionCreationDefaults; defaultCollectionThroughput?: CollectionCreationDefaults;
flights?: readonly string[]; flights?: readonly string[];
selfServeType?: SelfServeType; }
export interface SelfServeFrameInputs {
selfServeType: SelfServeType;
databaseAccount: any;
subscriptionId: string;
resourceGroup: string;
authorizationToken: string;
csmEndpoint: string;
flights?: readonly string[];
} }
export interface CollectionCreationDefaults { export interface CollectionCreationDefaults {

View File

@ -1,4 +1,4 @@
import { StringUtils } from "../../../Utils/StringUtils"; import * as StringUtils from "../../../Utils/StringUtils";
import { KeyCodes } from "../../../Common/Constants"; import { KeyCodes } from "../../../Common/Constants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";

View File

@ -4,7 +4,7 @@
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import { StringUtils } from "../../../Utils/StringUtils"; import * as StringUtils from "../../../Utils/StringUtils";
import { userContext } from "../../../UserContext"; import { userContext } from "../../../UserContext";
import { TerminalQueryParams } from "../../../Common/Constants"; import { TerminalQueryParams } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils"; import { handleError } from "../../../Common/ErrorHandlingUtils";

View File

@ -13,6 +13,8 @@ import {
LinkBase, LinkBase,
Separator, Separator,
TooltipHost, TooltipHost,
Spinner,
SpinnerSize,
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGalleryItem } from "../../../../Juno/JunoClient"; import { IGalleryItem } from "../../../../Juno/JunoClient";
@ -29,10 +31,14 @@ export interface GalleryCardComponentProps {
onFavoriteClick: () => void; onFavoriteClick: () => void;
onUnfavoriteClick: () => void; onUnfavoriteClick: () => void;
onDownloadClick: () => void; onDownloadClick: () => void;
onDeleteClick: () => void; onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void;
} }
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> { interface GalleryCardComponentState {
isDeletingPublishedNotebook: boolean;
}
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps, GalleryCardComponentState> {
public static readonly CARD_WIDTH = 256; public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144; private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio = public static readonly cardHeightToWidthRatio =
@ -40,6 +46,14 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
private static readonly cardDescriptionMaxChars = 80; private static readonly cardDescriptionMaxChars = 80;
private static readonly cardItemGapBig = 10; private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8; private static readonly cardItemGapSmall = 8;
private static readonly cardDeleteSpinnerHeight = 360;
constructor(props: GalleryCardComponentProps) {
super(props);
this.state = {
isDeletingPublishedNotebook: false,
};
}
public render(): JSX.Element { public render(): JSX.Element {
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete; const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
@ -59,6 +73,17 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }} tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
onClick={(event) => this.onClick(event, this.props.onClick)} onClick={(event) => this.onClick(event, this.props.onClick)}
> >
{this.state.isDeletingPublishedNotebook && (
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
<Spinner
size={SpinnerSize.large}
label={`Deleting '${cardTitle}'`}
styles={{ root: { height: GalleryCardComponent.cardDeleteSpinnerHeight } }}
/>
</Card.Item>
)}
{!this.state.isDeletingPublishedNotebook && (
<>
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}> <Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
<Persona <Persona
imageUrl={this.props.data.isSample && CosmosDBLogo} imageUrl={this.props.data.isSample && CosmosDBLogo}
@ -109,7 +134,8 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
</Text> </Text>
<span> <span>
{this.props.data.views !== undefined && this.generateIconText("RedEye", this.props.data.views.toString())} {this.props.data.views !== undefined &&
this.generateIconText("RedEye", this.props.data.views.toString())}
{this.props.data.downloads !== undefined && {this.props.data.downloads !== undefined &&
this.generateIconText("Download", this.props.data.downloads.toString())} this.generateIconText("Download", this.props.data.downloads.toString())}
{this.props.data.favorites !== undefined && {this.props.data.favorites !== undefined &&
@ -141,10 +167,17 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)} this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
{this.props.showDelete && {this.props.showDelete &&
this.generateIconButtonWithTooltip("Delete", "Remove", "right", this.props.onDeleteClick)} this.generateIconButtonWithTooltip("Delete", "Remove", "right", () =>
this.props.onDeleteClick(
() => this.setState({ isDeletingPublishedNotebook: true }),
() => this.setState({ isDeletingPublishedNotebook: false })
)
)}
</span> </span>
</Card.Section> </Card.Section>
)} )}
</>
)}
</Card> </Card>
); );
} }

View File

@ -11,7 +11,7 @@
.publicGalleryTabContainer { .publicGalleryTabContainer {
position: relative; position: relative;
height: 100vh; min-height: 100vh;
} }
.publicGalleryTabOverlayContent { .publicGalleryTabOverlayContent {

View File

@ -47,8 +47,8 @@ export interface GalleryViewerComponentProps {
} }
export enum GalleryTab { export enum GalleryTab {
OfficialSamples,
PublicGallery, PublicGallery,
OfficialSamples,
Favorites, Favorites,
Published, Published,
} }
@ -151,15 +151,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
public render(): JSX.Element { public render(): JSX.Element {
this.traceViewGallery(); this.traceViewGallery();
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [
tabs.push(
this.createPublicGalleryTab( this.createPublicGalleryTab(
GalleryTab.PublicGallery, GalleryTab.PublicGallery,
this.state.publicNotebooks, this.state.publicNotebooks,
this.state.isCodeOfConductAccepted this.state.isCodeOfConductAccepted
) ),
); this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks),
];
if (this.props.container) { if (this.props.container) {
tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks)); tabs.push(this.createFavoritesTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
@ -201,13 +200,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
switch (this.state.selectedTab) { switch (this.state.selectedTab) {
case GalleryTab.OfficialSamples:
if (!this.viewOfficialSamplesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewOfficialSamplesTraced = true;
trace(Action.NotebooksGalleryViewOfficialSamples);
}
break;
case GalleryTab.PublicGallery: case GalleryTab.PublicGallery:
if (!this.viewPublicGalleryTraced) { if (!this.viewPublicGalleryTraced) {
this.resetViewGalleryTabTracedFlags(); this.resetViewGalleryTabTracedFlags();
@ -215,6 +207,13 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
trace(Action.NotebooksGalleryViewPublicGallery); trace(Action.NotebooksGalleryViewPublicGallery);
} }
break; break;
case GalleryTab.OfficialSamples:
if (!this.viewOfficialSamplesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewOfficialSamplesTraced = true;
trace(Action.NotebooksGalleryViewOfficialSamples);
}
break;
case GalleryTab.Favorites: case GalleryTab.Favorites:
if (!this.viewFavoritesTraced) { if (!this.viewFavoritesTraced) {
this.resetViewGalleryTabTracedFlags(); this.resetViewGalleryTabTracedFlags();
@ -389,7 +388,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createSearchBarHeader(content: JSX.Element): JSX.Element { private createSearchBarHeader(content: JSX.Element): JSX.Element {
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}> <Stack horizontal wrap tokens={{ childrenGap: 20, padding: 10 }}>
<Stack.Item grow> <Stack.Item grow>
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} /> <SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
</Stack.Item> </Stack.Item>
@ -444,14 +443,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void { private loadTabContent(tab: GalleryTab, searchText: string, sortBy: SortBy, offline: boolean): void {
switch (tab) { switch (tab) {
case GalleryTab.OfficialSamples:
this.loadSampleNotebooks(searchText, sortBy, offline);
break;
case GalleryTab.PublicGallery: case GalleryTab.PublicGallery:
this.loadPublicNotebooks(searchText, sortBy, offline); this.loadPublicNotebooks(searchText, sortBy, offline);
break; break;
case GalleryTab.OfficialSamples:
this.loadSampleNotebooks(searchText, sortBy, offline);
break;
case GalleryTab.Favorites: case GalleryTab.Favorites:
this.loadFavoriteNotebooks(searchText, sortBy, offline); this.loadFavoriteNotebooks(searchText, sortBy, offline);
break; break;
@ -666,7 +665,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
onFavoriteClick: () => this.favoriteItem(data), onFavoriteClick: () => this.favoriteItem(data),
onUnfavoriteClick: () => this.unfavoriteItem(data), onUnfavoriteClick: () => this.unfavoriteItem(data),
onDownloadClick: () => this.downloadItem(data), onDownloadClick: () => this.downloadItem(data),
onDeleteClick: () => this.deleteItem(data), onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) =>
this.deleteItem(data, beforeDelete, afterDelete),
}; };
return ( return (
@ -710,11 +710,18 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
); );
}; };
private deleteItem = async (data: IGalleryItem): Promise<void> => { private deleteItem = async (data: IGalleryItem, beforeDelete: () => void, afterDelete: () => void): Promise<void> => {
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, (item) => { GalleryUtils.deleteItem(
this.props.container,
this.props.junoClient,
data,
(item) => {
this.publishedNotebooks = this.publishedNotebooks?.filter((notebook) => item.id !== notebook.id); this.publishedNotebooks = this.publishedNotebooks?.filter((notebook) => item.id !== notebook.id);
this.refreshSelectedTab(item); this.refreshSelectedTab(item);
}); },
beforeDelete,
afterDelete
);
}; };
private onPivotChange = (item: PivotItem): void => { private onPivotChange = (item: PivotItem): void => {

View File

@ -8,90 +8,6 @@ exports[`GalleryViewerComponent renders 1`] = `
onLinkClick={[Function]} onLinkClick={[Function]}
selectedKey="OfficialSamples" selectedKey="OfficialSamples"
> >
<PivotItem
headerText="Official samples"
itemKey="OfficialSamples"
key="OfficialSamples"
style={
Object {
"marginTop": 20,
}
}
>
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 20,
"padding": 10,
}
}
>
<StackItem
grow={true}
>
<StyledSearchBoxBase
onChange={[Function]}
placeholder="Search"
/>
</StackItem>
<StackItem>
<StyledLabelBase>
Sort by
</StyledLabelBase>
</StackItem>
<StackItem
styles={
Object {
"root": Object {
"minWidth": 200,
},
}
}
>
<StyledWithResponsiveMode
onChange={[Function]}
options={
Array [
Object {
"key": 0,
"text": "Most viewed",
},
Object {
"key": 1,
"text": "Most downloaded",
},
Object {
"key": 3,
"text": "Most recent",
},
Object {
"key": 2,
"text": "Most favorited",
},
]
}
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<StyledSpinnerBase
size={3}
/>
</StackItem>
</Stack>
</PivotItem>
<PivotItem <PivotItem
headerText="Public gallery" headerText="Public gallery"
itemKey="PublicGallery" itemKey="PublicGallery"
@ -120,6 +36,7 @@ exports[`GalleryViewerComponent renders 1`] = `
"padding": 10, "padding": 10,
} }
} }
wrap={true}
> >
<StackItem <StackItem
grow={true} grow={true}
@ -180,6 +97,91 @@ exports[`GalleryViewerComponent renders 1`] = `
</Stack> </Stack>
</div> </div>
</PivotItem> </PivotItem>
<PivotItem
headerText="Official samples"
itemKey="OfficialSamples"
key="OfficialSamples"
style={
Object {
"marginTop": 20,
}
}
>
<Stack
tokens={
Object {
"childrenGap": 10,
}
}
>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 20,
"padding": 10,
}
}
wrap={true}
>
<StackItem
grow={true}
>
<StyledSearchBoxBase
onChange={[Function]}
placeholder="Search"
/>
</StackItem>
<StackItem>
<StyledLabelBase>
Sort by
</StyledLabelBase>
</StackItem>
<StackItem
styles={
Object {
"root": Object {
"minWidth": 200,
},
}
}
>
<StyledWithResponsiveMode
onChange={[Function]}
options={
Array [
Object {
"key": 0,
"text": "Most viewed",
},
Object {
"key": 1,
"text": "Most downloaded",
},
Object {
"key": 3,
"text": "Most recent",
},
Object {
"key": 2,
"text": "Most favorited",
},
]
}
selectedKey={0}
/>
</StackItem>
<StackItem>
<InfoComponent />
</StackItem>
</Stack>
<StackItem>
<StyledSpinnerBase
size={3}
/>
</StackItem>
</Stack>
</PivotItem>
</StyledPivotBase> </StyledPivotBase>
</div> </div>
`; `;

View File

@ -15,7 +15,6 @@ import { Dialog, DialogProps, TextFieldProps } from "../Dialog";
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility"; import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils"; import { DialogHost } from "../../../Utils/GalleryUtils";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
@ -103,7 +102,7 @@ export class NotebookViewerComponent
); );
const notebook: Notebook = await response.json(); const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId); GalleryUtils.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook); this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook, showProgressBar: false }); this.setState({ content: notebook, showProgressBar: false });
@ -133,17 +132,6 @@ export class NotebookViewerComponent
} }
} }
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
delete notebookV4.cells[0];
notebook = notebookV4;
}
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookViewerContainer"> <div className="notebookViewerContainer">

View File

@ -962,13 +962,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"mostRecentActivity": MostRecentActivity {
"container": [Circular],
"storedData": Object {
"itemsMap": Object {},
"schemaVersion": "1",
},
},
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@ -1048,14 +1041,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@ -2159,13 +2144,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"mostRecentActivity": MostRecentActivity {
"container": [Circular],
"storedData": Object {
"itemsMap": Object {},
"schemaVersion": "1",
},
},
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@ -2245,14 +2223,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@ -3369,13 +3339,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"mostRecentActivity": MostRecentActivity {
"container": [Circular],
"storedData": Object {
"itemsMap": Object {},
"schemaVersion": "1",
},
},
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@ -3455,14 +3418,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,
@ -4566,13 +4521,6 @@ exports[`SettingsComponent renders 1`] = `
"visible": [Function], "visible": [Function],
}, },
"memoryUsageInfo": [Function], "memoryUsageInfo": [Function],
"mostRecentActivity": MostRecentActivity {
"container": [Circular],
"storedData": Object {
"itemsMap": Object {},
"schemaVersion": "1",
},
},
"newVertexPane": NewVertexPane { "newVertexPane": NewVertexPane {
"buildString": [Function], "buildString": [Function],
"container": [Circular], "container": [Circular],
@ -4652,14 +4600,6 @@ exports[`SettingsComponent renders 1`] = `
}, },
"selectedDatabaseId": [Function], "selectedDatabaseId": [Function],
"selectedNode": [Function], "selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function], "serverId": [Function],
"setInProgressConsoleDataIdToBeDeleted": undefined, "setInProgressConsoleDataIdToBeDeleted": undefined,
"setIsNotificationConsoleExpanded": undefined, "setIsNotificationConsoleExpanded": undefined,

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes"; import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => { describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = { const exampleData: SmartUiDescriptor = {
@ -18,10 +18,12 @@ describe("SmartUiComponent", () => {
{ {
id: "description", id: "description",
input: { input: {
labelTKey: undefined,
dataFieldName: "description", dataFieldName: "description",
type: "string", type: "string",
description: { description: {
textTKey: "this is an example description text.", textTKey: "this is an example description text.",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Click here for more information.", textTKey: "Click here for more information.",

View File

@ -6,12 +6,13 @@ import { Dropdown, IDropdownOption } from "office-ui-fabric-react/lib/Dropdown";
import { TextField } from "office-ui-fabric-react/lib/TextField"; import { TextField } from "office-ui-fabric-react/lib/TextField";
import { Text } from "office-ui-fabric-react/lib/Text"; import { Text } from "office-ui-fabric-react/lib/Text";
import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack"; import { Stack, IStackTokens } from "office-ui-fabric-react/lib/Stack";
import { Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react"; import { Label, Link, MessageBar, MessageBarType, Toggle } from "office-ui-fabric-react";
import * as InputUtils from "./InputUtils"; import * as InputUtils from "./InputUtils";
import "./SmartUiComponent.less"; import "./SmartUiComponent.less";
import { import {
ChoiceItem, ChoiceItem,
Description, Description,
DescriptionType,
Info, Info,
InputType, InputType,
InputTypeValue, InputTypeValue,
@ -19,6 +20,7 @@ import {
SmartUiInput, SmartUiInput,
} from "../../../SelfServe/SelfServeTypes"; } from "../../../SelfServe/SelfServeTypes";
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import { ToolTipLabelComponent } from "../Settings/SettingsSubComponents/ToolTipLabelComponent";
/** /**
* Generic UX renderer * Generic UX renderer
@ -29,15 +31,14 @@ import { TFunction } from "i18next";
*/ */
interface BaseDisplay { interface BaseDisplay {
labelTKey: string;
dataFieldName: string; dataFieldName: string;
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
} }
interface BaseInput extends BaseDisplay { interface BaseInput extends BaseDisplay {
labelTKey: string;
placeholderTKey?: string; placeholderTKey?: string;
errorMessage?: string;
} }
/** /**
@ -67,7 +68,8 @@ interface ChoiceInput extends BaseInput {
} }
interface DescriptionDisplay extends BaseDisplay { interface DescriptionDisplay extends BaseDisplay {
description: Description; description?: Description;
isDynamicDescription?: boolean;
} }
type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
@ -123,25 +125,27 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
private renderInfo(info: Info): JSX.Element { private renderInfo(info: Info): JSX.Element {
return ( return (
<MessageBar styles={{ root: { width: 400 } }}> info && (
{this.props.getTranslation(info.messageTKey)} <Text>
{this.props.getTranslation(info.messageTKey)}{" "}
{info.link && ( {info.link && (
<Link href={info.link.href} target="_blank"> <Link href={info.link.href} target="_blank">
{this.props.getTranslation(info.link.textTKey)} {this.props.getTranslation(info.link.textTKey)}
</Link> </Link>
)} )}
</MessageBar> </Text>
)
); );
} }
private renderTextInput(input: StringInput): JSX.Element { private renderTextInput(input: StringInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as string; const value = this.props.currentValues.get(input.dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<div className="stringInputContainer"> <div className="stringInputContainer">
<TextField <TextField
id={`${input.dataFieldName}-textField-input`} id={`${input.dataFieldName}-textField-input`}
label={this.props.getTranslation(input.labelTKey)} aria-labelledby={labelId}
type="text" type="text"
value={value || ""} value={value || ""}
placeholder={this.props.getTranslation(input.placeholderTKey)} placeholder={this.props.getTranslation(input.placeholderTKey)}
@ -149,32 +153,35 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(_, newValue) => this.props.onInputChange(input, newValue)} onChange={(_, newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
subComponentStyles: {
label: {
root: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
},
},
}} }}
/> />
</div> </div>
); );
} }
private renderDescription(input: DescriptionDisplay): JSX.Element { private renderDescription(input: DescriptionDisplay, labelId: string): JSX.Element {
const description = input.description; const dataFieldName = input.dataFieldName;
return ( const description = input.description || (this.props.currentValues.get(dataFieldName)?.value as Description);
<Text id={`${input.dataFieldName}-text-display`}> if (!description) {
{this.props.getTranslation(input.description.textTKey)}{" "} return this.renderError("Description is not provided.");
}
const descriptionElement = (
<Text id={`${dataFieldName}-text-display`} aria-labelledby={labelId}>
{this.props.getTranslation(description.textTKey)}{" "}
{description.link && ( {description.link && (
<Link target="_blank" href={input.description.link.href}> <Link target="_blank" href={description.link.href}>
{this.props.getTranslation(input.description.link.textTKey)} {this.props.getTranslation(description.link.textTKey)}
</Link> </Link>
)} )}
</Text> </Text>
); );
if (description.type === DescriptionType.Text) {
return descriptionElement;
}
const messageBarType =
description.type === DescriptionType.InfoMessageBar ? MessageBarType.info : MessageBarType.warning;
return <MessageBar messageBarType={messageBarType}>{descriptionElement}</MessageBar>;
} }
private clearError(dataFieldName: string): void { private clearError(dataFieldName: string): void {
@ -220,13 +227,12 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return undefined; return undefined;
}; };
private renderNumberInput(input: NumberInput): JSX.Element { private renderNumberInput(input: NumberInput, labelId: string): JSX.Element {
const { labelTKey, min, max, dataFieldName, step } = input; const { labelTKey, min, max, dataFieldName, step } = input;
const props = { const props = {
label: this.props.getTranslation(labelTKey),
min: min, min: min,
max: max, max: max,
ariaLabel: labelTKey, ariaLabel: this.props.getTranslation(labelTKey),
step: step, step: step,
}; };
@ -243,13 +249,8 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)} onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)} onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
labelPosition={Position.top} labelPosition={Position.top}
aria-labelledby={labelId}
disabled={disabled} disabled={disabled}
styles={{
label: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
}}
/> />
{this.state.errors.has(dataFieldName) && ( {this.state.errors.has(dataFieldName) && (
<MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar> <MessageBar messageBarType={MessageBarType.error}>Error: {this.state.errors.get(dataFieldName)}</MessageBar>
@ -266,10 +267,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
onChange={(newValue) => this.props.onInputChange(input, newValue)} onChange={(newValue) => this.props.onInputChange(input, newValue)}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
titleLabel: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
valueLabel: SmartUiComponent.labelStyle, valueLabel: SmartUiComponent.labelStyle,
}} }}
/> />
@ -280,13 +277,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
} }
} }
private renderBooleanInput(input: BooleanInput): JSX.Element { private renderBooleanInput(input: BooleanInput, labelId: string): JSX.Element {
const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean; const value = this.props.currentValues.get(input.dataFieldName)?.value as boolean;
const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(input.dataFieldName)?.disabled;
return ( return (
<Toggle <Toggle
id={`${input.dataFieldName}-toggle-input`} id={`${input.dataFieldName}-toggle-input`}
label={this.props.getTranslation(input.labelTKey)} aria-labelledby={labelId}
checked={value || false} checked={value || false}
onText={this.props.getTranslation(input.trueLabelTKey)} onText={this.props.getTranslation(input.trueLabelTKey)}
offText={this.props.getTranslation(input.falseLabelTKey)} offText={this.props.getTranslation(input.falseLabelTKey)}
@ -297,8 +294,8 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
); );
} }
private renderChoiceInput(input: ChoiceInput): JSX.Element { private renderChoiceInput(input: ChoiceInput, labelId: string): JSX.Element {
const { labelTKey, defaultKey, dataFieldName, choices, placeholderTKey } = input; const { defaultKey, dataFieldName, choices, placeholderTKey } = input;
const value = this.props.currentValues.get(dataFieldName)?.value as string; const value = this.props.currentValues.get(dataFieldName)?.value as string;
const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled; const disabled = this.props.disabled || this.props.currentValues.get(dataFieldName)?.disabled;
let selectedKey = value ? value : defaultKey; let selectedKey = value ? value : defaultKey;
@ -308,7 +305,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Dropdown <Dropdown
id={`${input.dataFieldName}-dropdown-input`} id={`${input.dataFieldName}-dropdown-input`}
label={this.props.getTranslation(labelTKey)} aria-labelledby={labelId}
selectedKey={selectedKey} selectedKey={selectedKey}
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())} onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
placeholder={this.props.getTranslation(placeholderTKey)} placeholder={this.props.getTranslation(placeholderTKey)}
@ -319,40 +316,53 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}))} }))}
styles={{ styles={{
root: { width: 400 }, root: { width: 400 },
label: {
...SmartUiComponent.labelStyle,
fontWeight: 600,
},
dropdown: SmartUiComponent.labelStyle, dropdown: SmartUiComponent.labelStyle,
}} }}
/> />
); );
} }
private renderError(input: AnyDisplay): JSX.Element { private renderError(errorMessage: string): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>; return <MessageBar messageBarType={MessageBarType.error}>Error: {errorMessage}</MessageBar>;
} }
private renderDisplay(input: AnyDisplay): JSX.Element { private renderDisplayWithInfoBubble(input: AnyDisplay, info: Info): JSX.Element {
if (input.errorMessage) { if (input.errorMessage) {
return this.renderError(input); return this.renderError(input.errorMessage);
} }
const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden; const inputHidden = this.props.currentValues.get(input.dataFieldName)?.hidden;
if (inputHidden) { if (inputHidden) {
return <></>; return <></>;
} }
const labelId = `${input.dataFieldName}-label`;
return (
<Stack>
{input.labelTKey && (
<Label id={labelId}>
<ToolTipLabelComponent
label={this.props.getTranslation(input.labelTKey)}
toolTipElement={this.renderInfo(info)}
/>
</Label>
)}
{this.renderDisplay(input, labelId)}
</Stack>
);
}
private renderDisplay(input: AnyDisplay, labelId: string): JSX.Element {
switch (input.type) { switch (input.type) {
case "string": case "string":
if ("description" in input) { if ("description" in input || "isDynamicDescription" in input) {
return this.renderDescription(input as DescriptionDisplay); return this.renderDescription(input as DescriptionDisplay, labelId);
} }
return this.renderTextInput(input as StringInput); return this.renderTextInput(input as StringInput, labelId);
case "number": case "number":
return this.renderNumberInput(input as NumberInput); return this.renderNumberInput(input as NumberInput, labelId);
case "boolean": case "boolean":
return this.renderBooleanInput(input as BooleanInput); return this.renderBooleanInput(input as BooleanInput, labelId);
case "object": case "object":
return this.renderChoiceInput(input as ChoiceInput); return this.renderChoiceInput(input as ChoiceInput, labelId);
default: default:
throw new Error(`Unknown input type: ${input.type}`); throw new Error(`Unknown input type: ${input.type}`);
} }
@ -363,10 +373,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return ( return (
<Stack tokens={containerStackTokens} className="widgetRendererContainer"> <Stack tokens={containerStackTokens} className="widgetRendererContainer">
<Stack.Item> <Stack.Item>{node.input && this.renderDisplayWithInfoBubble(node.input, node.info as Info)}</Stack.Item>
{node.info && this.renderInfo(node.info as Info)}
{node.input && this.renderDisplay(node.input)}
</Stack.Item>
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)} {node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
</Stack> </Stack>
); );

View File

@ -9,25 +9,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@ -40,7 +22,9 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<Text <Text
aria-labelledby="description-label"
id="description-text-display" id="description-text-display"
> >
this is an example description text. this is an example description text.
@ -52,6 +36,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
Click here for more information. Click here for more information.
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -67,6 +52,14 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="throughput-label"
>
<ToolTipLabelComponent
label="Throughput (input)"
/>
</StyledLabelBase>
<Stack <Stack
styles={ styles={
Object { Object {
@ -82,6 +75,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<CustomizedSpinButton <CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)" ariaLabel="Throughput (input)"
decrementButtonIcon={ decrementButtonIcon={
Object { Object {
@ -95,7 +89,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
"iconName": "ChevronUpSmall", "iconName": "ChevronUpSmall",
} }
} }
label="Throughput (input)" label=""
labelPosition={0} labelPosition={0}
max={500} max={500}
min={400} min={400}
@ -103,18 +97,9 @@ exports[`SmartUiComponent disable all inputs 1`] = `
onIncrement={[Function]} onIncrement={[Function]}
onValidate={[Function]} onValidate={[Function]}
step={10} step={10}
styles={
Object {
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/> />
</Stack> </Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -130,13 +115,20 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="throughput2-label"
>
<ToolTipLabelComponent
label="Throughput (Slider)"
/>
</StyledLabelBase>
<div <div
id="throughput2-slider-input" id="throughput2-slider-input"
> >
<StyledSliderBase <StyledSliderBase
ariaLabel="Throughput (Slider)" ariaLabel="Throughput (Slider)"
disabled={true} disabled={true}
label="Throughput (Slider)"
max={500} max={500}
min={400} min={400}
onChange={[Function]} onChange={[Function]}
@ -146,12 +138,6 @@ exports[`SmartUiComponent disable all inputs 1`] = `
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object { "valueLabel": Object {
"color": "#393939", "color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
@ -161,6 +147,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
/> />
</div> </div>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -197,35 +184,34 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="containerId-label"
>
<ToolTipLabelComponent
label="Container id"
/>
</StyledLabelBase>
<div <div
className="stringInputContainer" className="stringInputContainer"
> >
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label"
disabled={true} disabled={true}
id="containerId-textField-input" id="containerId-textField-input"
label="Container id"
onChange={[Function]} onChange={[Function]}
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
} }
} }
type="text" type="text"
value="" value=""
/> />
</div> </div>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -241,11 +227,19 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="analyticalStore-label"
>
<ToolTipLabelComponent
label="Analytical Store"
/>
</StyledLabelBase>
<StyledToggleBase <StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false} checked={false}
disabled={true} disabled={true}
id="analyticalStore-toggle-input" id="analyticalStore-toggle-input"
label="Analytical Store"
offText="Disabled" offText="Disabled"
onChange={[Function]} onChange={[Function]}
onText="Enabled" onText="Enabled"
@ -257,6 +251,7 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
} }
/> />
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -272,10 +267,18 @@ exports[`SmartUiComponent disable all inputs 1`] = `
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="database-label"
>
<ToolTipLabelComponent
label="Database"
/>
</StyledLabelBase>
<StyledWithResponsiveMode <StyledWithResponsiveMode
aria-labelledby="database-label"
disabled={true} disabled={true}
id="database-dropdown-input" id="database-dropdown-input"
label="Database"
onChange={[Function]} onChange={[Function]}
options={ options={
Array [ Array [
@ -301,18 +304,13 @@ exports[`SmartUiComponent disable all inputs 1`] = `
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12, "fontSize": 12,
}, },
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
} }
} }
/> />
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -328,25 +326,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
} }
> >
<StackItem> <StackItem />
<StyledMessageBarBase
styles={
Object {
"root": Object {
"width": 400,
},
}
}
>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div <div
key="description" key="description"
> >
@ -359,7 +339,9 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack>
<Text <Text
aria-labelledby="description-label"
id="description-text-display" id="description-text-display"
> >
this is an example description text. this is an example description text.
@ -371,6 +353,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
Click here for more information. Click here for more information.
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -386,6 +369,14 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="throughput-label"
>
<ToolTipLabelComponent
label="Throughput (input)"
/>
</StyledLabelBase>
<Stack <Stack
styles={ styles={
Object { Object {
@ -401,6 +392,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<CustomizedSpinButton <CustomizedSpinButton
aria-labelledby="throughput-label"
ariaLabel="Throughput (input)" ariaLabel="Throughput (input)"
decrementButtonIcon={ decrementButtonIcon={
Object { Object {
@ -414,7 +406,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
"iconName": "ChevronUpSmall", "iconName": "ChevronUpSmall",
} }
} }
label="Throughput (input)" label=""
labelPosition={0} labelPosition={0}
max={500} max={500}
min={400} min={400}
@ -422,18 +414,9 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
onIncrement={[Function]} onIncrement={[Function]}
onValidate={[Function]} onValidate={[Function]}
step={10} step={10}
styles={
Object {
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/> />
</Stack> </Stack>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -449,12 +432,19 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="throughput2-label"
>
<ToolTipLabelComponent
label="Throughput (Slider)"
/>
</StyledLabelBase>
<div <div
id="throughput2-slider-input" id="throughput2-slider-input"
> >
<StyledSliderBase <StyledSliderBase
ariaLabel="Throughput (Slider)" ariaLabel="Throughput (Slider)"
label="Throughput (Slider)"
max={500} max={500}
min={400} min={400}
onChange={[Function]} onChange={[Function]}
@ -464,12 +454,6 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object { "valueLabel": Object {
"color": "#393939", "color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
@ -479,6 +463,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
/> />
</div> </div>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -515,34 +500,33 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="containerId-label"
>
<ToolTipLabelComponent
label="Container id"
/>
</StyledLabelBase>
<div <div
className="stringInputContainer" className="stringInputContainer"
> >
<StyledTextFieldBase <StyledTextFieldBase
aria-labelledby="containerId-label"
id="containerId-textField-input" id="containerId-textField-input"
label="Container id"
onChange={[Function]} onChange={[Function]}
styles={ styles={
Object { Object {
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
} }
} }
type="text" type="text"
value="" value=""
/> />
</div> </div>
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -558,10 +542,18 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<Stack>
<StyledLabelBase
id="analyticalStore-label"
>
<ToolTipLabelComponent
label="Analytical Store"
/>
</StyledLabelBase>
<StyledToggleBase <StyledToggleBase
aria-labelledby="analyticalStore-label"
checked={false} checked={false}
id="analyticalStore-toggle-input" id="analyticalStore-toggle-input"
label="Analytical Store"
offText="Disabled" offText="Disabled"
onChange={[Function]} onChange={[Function]}
onText="Enabled" onText="Enabled"
@ -573,6 +565,7 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
} }
/> />
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>
@ -588,9 +581,17 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
} }
> >
<StackItem> <StackItem>
<StyledWithResponsiveMode <Stack>
id="database-dropdown-input" <StyledLabelBase
id="database-label"
>
<ToolTipLabelComponent
label="Database" label="Database"
/>
</StyledLabelBase>
<StyledWithResponsiveMode
aria-labelledby="database-label"
id="database-dropdown-input"
onChange={[Function]} onChange={[Function]}
options={ options={
Array [ Array [
@ -616,18 +617,13 @@ exports[`SmartUiComponent should render and honor input's hidden, disabled state
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif", "fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12, "fontSize": 12,
}, },
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"root": Object { "root": Object {
"width": 400, "width": 400,
}, },
} }
} }
/> />
</Stack>
</StackItem> </StackItem>
</Stack> </Stack>
</div> </div>

View File

@ -129,7 +129,6 @@ export interface ThroughputInputParams {
throughputModeRadioName: string; throughputModeRadioName: string;
maxAutoPilotThroughputSet: ViewModels.Editable<number>; maxAutoPilotThroughputSet: ViewModels.Editable<number>;
autoPilotUsageCost: ko.Computed<string>; autoPilotUsageCost: ko.Computed<string>;
showAutoPilot?: ko.Observable<boolean>;
overrideWithAutoPilotSettings: ko.Observable<boolean>; overrideWithAutoPilotSettings: ko.Observable<boolean>;
overrideWithProvisionedThroughputSettings: ko.Observable<boolean>; overrideWithProvisionedThroughputSettings: ko.Observable<boolean>;
freeTierExceedThroughputTooltip?: ko.Observable<string>; freeTierExceedThroughputTooltip?: ko.Observable<string>;
@ -158,7 +157,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
public infoBubbleText: string | ko.Observable<string>; public infoBubbleText: string | ko.Observable<string>;
public label: ko.Observable<string>; public label: ko.Observable<string>;
public isFixed: boolean; public isFixed: boolean;
public showAutoPilot: ko.Observable<boolean>;
public isAutoPilotSelected: ko.Observable<boolean>; public isAutoPilotSelected: ko.Observable<boolean>;
public throughputAutoPilotRadioId: string; public throughputAutoPilotRadioId: string;
public throughputProvisionedRadioId: string; public throughputProvisionedRadioId: string;
@ -202,7 +200,6 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
this.isFixed = !!options.isFixed; this.isFixed = !!options.isFixed;
this.infoBubbleText = options.infoBubbleText || ko.observable<string>(); this.infoBubbleText = options.infoBubbleText || ko.observable<string>();
this.label = options.label || ko.observable<string>(); this.label = options.label || ko.observable<string>();
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false); this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
this.isAutoPilotSelected.subscribe((value) => { this.isAutoPilotSelected.subscribe((value) => {
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, { TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {

View File

@ -17,7 +17,7 @@
</div> </div>
<!-- ko if: !isFixed --> <!-- ko if: !isFixed -->
<div data-bind="visible: showAutoPilot" class="throughputModeContainer"> <div class="throughputModeContainer">
<input <input
class="throughputModeRadio" class="throughputModeRadio"
aria-label="Autopilot mode" aria-label="Autopilot mode"

View File

@ -61,6 +61,7 @@ describe("ContainerSampleGenerator", () => {
const database = { const database = {
id: ko.observable(sampleDatabaseId), id: ko.observable(sampleDatabaseId),
collections: ko.observableArray<ViewModels.Collection>([collection]), collections: ko.observableArray<ViewModels.Collection>([collection]),
loadCollections: () => {},
} as ViewModels.Database; } as ViewModels.Database;
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
@ -109,6 +110,7 @@ describe("ContainerSampleGenerator", () => {
const database = { const database = {
id: ko.observable(sampleDatabaseId), id: ko.observable(sampleDatabaseId),
collections: ko.observableArray<ViewModels.Collection>([collection]), collections: ko.observableArray<ViewModels.Collection>([collection]),
loadCollections: () => {},
} as ViewModels.Database; } as ViewModels.Database;
database.findCollectionWithId = () => collection; database.findCollectionWithId = () => collection;
collection.databaseId = database.id(); collection.databaseId = database.id();

View File

@ -63,6 +63,7 @@ export class ContainerSampleGenerator {
if (!database) { if (!database) {
return undefined; return undefined;
} }
await database.loadCollections();
return database.findCollectionWithId(this.sampleDataFile.collectionId); return database.findCollectionWithId(this.sampleDataFile.collectionId);
} }

View File

@ -1,93 +1,86 @@
import React from "react";
import * as ComponentRegisterer from "./ComponentRegisterer";
import * as Constants from "../Common/Constants";
import * as DataModels from "../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as MostRecentActivity from "./MostRecentActivity/MostRecentActivity"; import { IChoiceGroupProps } from "office-ui-fabric-react";
import * as path from "path"; import * as path from "path";
import * as SharedConstants from "../Shared/Constants";
import * as ViewModels from "../Contracts/ViewModels";
import _ from "underscore";
import AddCollectionPane from "./Panes/AddCollectionPane";
import AddDatabasePane from "./Panes/AddDatabasePane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import AuthHeadersUtil from "../Platform/Hosted/Authorization";
import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import Database from "./Tree/Database";
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { readCollection } from "../Common/dataAccess/readCollection";
import { readDatabases } from "../Common/dataAccess/readDatabases";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import GraphStylingPane from "./Panes/GraphStylingPane";
import NewVertexPane from "./Panes/NewVertexPane";
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import Q from "q"; import Q from "q";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection"; import React from "react";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor"; import _ from "underscore";
import TerminalTab from "./Tabs/TerminalTab";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane"; import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient"; import * as Constants from "../Common/Constants";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { configContext, Platform, updateConfigContext } from "../ConfigContext";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import { ExplorerMetrics } from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants";
import { ExplorerSettings } from "../Shared/ExplorerSettings"; import { readCollection } from "../Common/dataAccess/readCollection";
import { FileSystemUtil } from "./Notebook/FileSystemUtil"; import { readDatabases } from "../Common/dataAccess/readDatabases";
import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient"; import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { sendMessage, sendCachedDataMessage } from "../Common/MessageHandler"; import { sendCachedDataMessage, sendMessage } from "../Common/MessageHandler";
import { QueriesClient } from "../Common/QueriesClient";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { configContext, Platform } from "../ConfigContext";
import * as DataModels from "../Contracts/DataModels";
import { MessageTypes } from "../Contracts/ExplorerContracts";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import * as ViewModels from "../Contracts/ViewModels";
import { IGalleryItem, IPinnedRepo } from "../Juno/JunoClient";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
import { RouteHandler } from "../RouteHandlers/RouteHandler";
import { appInsights } from "../Shared/appInsights";
import * as SharedConstants from "../Shared/Constants";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { ExplorerSettings } from "../Shared/ExplorerSettings";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
import { ArcadiaResourceManager } from "../SparkClusterManager/ArcadiaResourceManager";
import { updateUserContext, userContext } from "../UserContext";
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { stringToBlob } from "../Utils/BlobUtils";
import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import * as ComponentRegisterer from "./ComponentRegisterer";
import { ArcadiaWorkspaceItem } from "./Controls/Arcadia/ArcadiaMenuPicker";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { DialogProps, TextFieldProps } from "./Controls/Dialog";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
import { FileSystemUtil } from "./Notebook/FileSystemUtil";
import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./Notebook/NotebookContentItem";
import { NotebookUtil } from "./Notebook/NotebookUtil"; import { NotebookUtil } from "./Notebook/NotebookUtil";
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager"; import AddCollectionPane from "./Panes/AddCollectionPane";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils"; import AddDatabasePane from "./Panes/AddDatabasePane";
import { QueriesClient } from "../Common/QueriesClient"; import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane"; import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory"; import { ContextualPaneBase } from "./Panes/ContextualPaneBase";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken"; import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
import { RouteHandler } from "../RouteHandlers/RouteHandler"; import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane";
import GraphStylingPane from "./Panes/GraphStylingPane";
import { LoadQueryPane } from "./Panes/LoadQueryPane";
import NewVertexPane from "./Panes/NewVertexPane";
import { SaveQueryPane } from "./Panes/SaveQueryPane"; import { SaveQueryPane } from "./Panes/SaveQueryPane";
import { SettingsPane } from "./Panes/SettingsPane"; import { SettingsPane } from "./Panes/SettingsPane";
import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane"; import { SetupNotebooksPane } from "./Panes/SetupNotebooksPane";
import { SplashScreen } from "./SplashScreen/SplashScreen";
import { Splitter, SplitterBounds, SplitterDirection } from "../Common/Splitter";
import { StringInputPane } from "./Panes/StringInputPane"; import { StringInputPane } from "./Panes/StringInputPane";
import AddTableEntityPane from "./Panes/Tables/AddTableEntityPane";
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
import { QuerySelectPane } from "./Panes/Tables/QuerySelectPane";
import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane"; import { TableColumnOptionsPane } from "./Panes/Tables/TableColumnOptionsPane";
import { TabsManager } from "./Tabs/TabsManager";
import { UploadFilePane } from "./Panes/UploadFilePane"; import { UploadFilePane } from "./Panes/UploadFilePane";
import { UploadItemsPane } from "./Panes/UploadItemsPane"; import { UploadItemsPane } from "./Panes/UploadItemsPane";
import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter"; import { UploadItemsPaneAdapter } from "./Panes/UploadItemsPaneAdapter";
import { ReactAdapter } from "../Bindings/ReactBindingHandler"; import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
import { toRawContentUri, fromContentUri } from "../Utils/GitHubUtils"; import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
import UserDefinedFunction from "./Tree/UserDefinedFunction"; import TabsBase from "./Tabs/TabsBase";
import { TabsManager } from "./Tabs/TabsManager";
import TerminalTab from "./Tabs/TerminalTab";
import Database from "./Tree/Database";
import ResourceTokenCollection from "./Tree/ResourceTokenCollection";
import { ResourceTreeAdapterForResourceToken } from "./Tree/ResourceTreeAdapterForResourceToken";
import StoredProcedure from "./Tree/StoredProcedure"; import StoredProcedure from "./Tree/StoredProcedure";
import Trigger from "./Tree/Trigger"; import Trigger from "./Tree/Trigger";
import { ContextualPaneBase } from "./Panes/ContextualPaneBase"; import UserDefinedFunction from "./Tree/UserDefinedFunction";
import TabsBase from "./Tabs/TabsBase";
import { CommandButtonComponentProps } from "./Controls/CommandButton/CommandButtonComponent";
import { updateUserContext, userContext } from "../UserContext";
import { stringToBlob } from "../Utils/BlobUtils";
import { IChoiceGroupProps } from "office-ui-fabric-react";
import { getErrorMessage, handleError, getErrorStack } from "../Common/ErrorHandlingUtils";
import { SubscriptionType } from "../Contracts/SubscriptionType";
import { appInsights } from "../Shared/appInsights";
import { SelfServeLoadingComponentAdapter } from "../SelfServe/SelfServeLoadingComponentAdapter";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { SelfServeComponentAdapter } from "../SelfServe/SelfServeComponentAdapter";
import { GalleryTab } from "./Controls/NotebookGallery/GalleryViewerComponent";
import { DeleteCollectionConfirmationPanel } from "./Panes/DeleteCollectionConfirmationPanel";
BindingHandlersRegisterer.registerBindingHandlers(); BindingHandlersRegisterer.registerBindingHandlers();
// Hold a reference to ComponentRegisterer to prevent transpiler to ignore import // Hold a reference to ComponentRegisterer to prevent transpiler to ignore import
@ -122,20 +115,55 @@ export default class Explorer {
public hasWriteAccess: ko.Observable<boolean>; public hasWriteAccess: ko.Observable<boolean>;
public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth; public collapsedResourceTreeWidth: number = ExplorerMetrics.CollapsedResourceTreeWidth;
/**
* @deprecated
* Use userContext.databaseAccount instead
* */
public databaseAccount: ko.Observable<DataModels.DatabaseAccount>; public databaseAccount: ko.Observable<DataModels.DatabaseAccount>;
public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults; public collectionCreationDefaults: ViewModels.CollectionCreationDefaults = SharedConstants.CollectionCreationDefaults;
/**
* @deprecated
* Use userContext.subscriptionType instead
* */
public subscriptionType: ko.Observable<SubscriptionType>; public subscriptionType: ko.Observable<SubscriptionType>;
/**
* @deprecated
* Use userContext.apiType instead
* */
public defaultExperience: ko.Observable<string>; public defaultExperience: ko.Observable<string>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "SQL"
* */
public isPreferredApiDocumentDB: ko.Computed<boolean>; public isPreferredApiDocumentDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Cassandra"
* */
public isPreferredApiCassandra: ko.Computed<boolean>; public isPreferredApiCassandra: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isPreferredApiMongoDB: ko.Computed<boolean>; public isPreferredApiMongoDB: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Gremlin"
* */
public isPreferredApiGraph: ko.Computed<boolean>; public isPreferredApiGraph: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Tables"
* */
public isPreferredApiTable: ko.Computed<boolean>; public isPreferredApiTable: ko.Computed<boolean>;
public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>; public isFixedCollectionWithSharedThroughputSupported: ko.Computed<boolean>;
/**
* @deprecated
* Compare a string with userContext.apiType instead: userContext.apiType === "Mongo"
* */
public isEnableMongoCapabilityPresent: ko.Computed<boolean>; public isEnableMongoCapabilityPresent: ko.Computed<boolean>;
public isServerlessEnabled: ko.Computed<boolean>; public isServerlessEnabled: ko.Computed<boolean>;
public isAccountReady: ko.Observable<boolean>; public isAccountReady: ko.Observable<boolean>;
public selfServeType: ko.Observable<SelfServeType>;
public canSaveQueries: ko.Computed<boolean>; public canSaveQueries: ko.Computed<boolean>;
public features: ko.Observable<any>; public features: ko.Observable<any>;
public serverId: ko.Observable<string>; public serverId: ko.Observable<string>;
@ -143,7 +171,6 @@ export default class Explorer {
public queriesClient: QueriesClient; public queriesClient: QueriesClient;
public tableDataClient: TableDataClient; public tableDataClient: TableDataClient;
public splitter: Splitter; public splitter: Splitter;
public mostRecentActivity: MostRecentActivity.MostRecentActivity;
// Notification Console // Notification Console
private setIsNotificationConsoleExpanded: (isExpanded: boolean) => void; private setIsNotificationConsoleExpanded: (isExpanded: boolean) => void;
@ -162,8 +189,11 @@ export default class Explorer {
public selectedCollectionId: ko.Computed<string>; public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>; public isLeftPaneExpanded: ko.Observable<boolean>;
public selectedNode: ko.Observable<ViewModels.TreeNode>; public selectedNode: ko.Observable<ViewModels.TreeNode>;
/**
* @deprecated
* Use a local loading state and spinner instead. Using a global isRefreshing state causes problems.
* */
public isRefreshingExplorer: ko.Observable<boolean>; public isRefreshingExplorer: ko.Observable<boolean>;
private selfServeComponentAdapter: SelfServeComponentAdapter;
// Resource Token // Resource Token
public resourceTokenDatabaseId: ko.Observable<string>; public resourceTokenDatabaseId: ko.Observable<string>;
@ -247,7 +277,6 @@ export default class Explorer {
// React adapters // React adapters
private commandBarComponentAdapter: CommandBarComponentAdapter; private commandBarComponentAdapter: CommandBarComponentAdapter;
private selfServeLoadingComponentAdapter: SelfServeLoadingComponentAdapter;
private static readonly MaxNbDatabasesToAutoExpand = 5; private static readonly MaxNbDatabasesToAutoExpand = 5;
@ -291,7 +320,6 @@ export default class Explorer {
} }
}); });
this.isAccountReady = ko.observable<boolean>(false); this.isAccountReady = ko.observable<boolean>(false);
this.selfServeType = ko.observable<SelfServeType>(undefined);
this._isInitializingNotebooks = false; this._isInitializingNotebooks = false;
this.arcadiaToken = ko.observable<string>(); this.arcadiaToken = ko.observable<string>();
this.arcadiaToken.subscribe((token: string) => { this.arcadiaToken.subscribe((token: string) => {
@ -443,6 +471,7 @@ export default class Explorer {
databaseAccount databaseAccount
); );
this.defaultExperience(defaultExperience); this.defaultExperience(defaultExperience);
// TODO. Remove this entirely
updateUserContext({ updateUserContext({
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience), defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience),
}); });
@ -666,7 +695,6 @@ export default class Explorer {
}); });
this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this); this.uploadItemsPaneAdapter = new UploadItemsPaneAdapter(this);
this.selfServeComponentAdapter = new SelfServeComponentAdapter(this);
this.loadQueryPane = new LoadQueryPane({ this.loadQueryPane = new LoadQueryPane({
id: "loadquerypane", id: "loadquerypane",
@ -841,7 +869,6 @@ export default class Explorer {
}); });
this.commandBarComponentAdapter = new CommandBarComponentAdapter(this); this.commandBarComponentAdapter = new CommandBarComponentAdapter(this);
this.selfServeLoadingComponentAdapter = new SelfServeLoadingComponentAdapter();
this._initSettings(); this._initSettings();
@ -924,8 +951,6 @@ export default class Explorer {
featureSubcription.dispose(); featureSubcription.dispose();
}); });
this.mostRecentActivity = new MostRecentActivity.MostRecentActivity(this);
} }
public openEnableSynapseLinkDialog(): void { public openEnableSynapseLinkDialog(): void {
@ -1411,20 +1436,6 @@ export default class Explorer {
return false; return false;
} }
public setSelfServeType(inputs: ViewModels.DataExplorerInputsFrame): void {
const selfServeFeature = inputs.features[Constants.Features.selfServeType];
if (selfServeFeature) {
// self serve type received from query string
const selfServeType = SelfServeType[selfServeFeature?.toLowerCase() as keyof typeof SelfServeType];
this.selfServeType(selfServeType ? selfServeType : SelfServeType.invalid);
} else if (inputs.selfServeType) {
// self serve type received from portal
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeType.none);
}
}
public configure(inputs: ViewModels.DataExplorerInputsFrame): void { public configure(inputs: ViewModels.DataExplorerInputsFrame): void {
if (inputs != null) { if (inputs != null) {
// 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.
@ -1433,8 +1444,6 @@ export default class Explorer {
sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs)); sessionStorage.setItem("portalDataExplorerInitMessage", JSON.stringify(inputs));
} }
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount || null; const databaseAccount = inputs.databaseAccount || null;
if (inputs.defaultCollectionThroughput) { if (inputs.defaultCollectionThroughput) {
this.collectionCreationDefaults = inputs.defaultCollectionThroughput; this.collectionCreationDefaults = inputs.defaultCollectionThroughput;
@ -1450,22 +1459,6 @@ export default class Explorer {
this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false); this.isTryCosmosDBSubscription(inputs.isTryCosmosDBSubscription ?? false);
this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false); this.isAuthWithResourceToken(inputs.isAuthWithresourceToken ?? false);
this.setFeatureFlagsFromFlights(inputs.flights); this.setFeatureFlagsFromFlights(inputs.flights);
this.setSelfServeType(inputs);
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
});
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadDatabaseAccount, Action.LoadDatabaseAccount,
{ {
@ -2332,7 +2325,7 @@ export default class Explorer {
account: userContext.databaseAccount, account: userContext.databaseAccount,
container: this, container: this,
junoClient: this.notebookManager?.junoClient, junoClient: this.notebookManager?.junoClient,
selectedTab: selectedTab || GalleryTab.OfficialSamples, selectedTab: selectedTab || GalleryTab.PublicGallery,
notebookUrl, notebookUrl,
galleryItem, galleryItem,
isFavorite, isFavorite,

View File

@ -5,7 +5,7 @@
import * as React from "react"; import * as React from "react";
import { NeighborVertexBasicInfo, EditedEdges, GraphNewEdgeData, PossibleVertex } from "./GraphExplorer"; import { NeighborVertexBasicInfo, EditedEdges, GraphNewEdgeData, PossibleVertex } from "./GraphExplorer";
import { GraphUtil } from "./GraphUtil"; import * as GraphUtil from "./GraphUtil";
import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent"; import * as InputTypeaheadComponent from "../../Controls/InputTypeahead/InputTypeaheadComponent";
import DeleteIcon from "../../../../images/delete.svg"; import DeleteIcon from "../../../../images/delete.svg";
import AddPropertyIcon from "../../../../images/Add-property.svg"; import AddPropertyIcon from "../../../../images/Add-property.svg";

View File

@ -9,7 +9,7 @@ import { GraphVizComponentProps } from "./GraphVizComponent";
import * as GraphData from "./GraphData"; import * as GraphData from "./GraphData";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
import { GraphUtil } from "./GraphUtil"; import * as GraphUtil from "./GraphUtil";
import * as DataModels from "../../../Contracts/DataModels"; import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import * as GremlinClient from "./GremlinClient"; import * as GremlinClient from "./GremlinClient";

View File

@ -1,4 +1,4 @@
import { GraphUtil } from "./GraphUtil"; import * as GraphUtil from "./GraphUtil";
import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData"; import { GraphData, GremlinVertex, GremlinEdge } from "./GraphData";
import * as sinon from "sinon"; import * as sinon from "sinon";
import { GraphExplorer } from "./GraphExplorer"; import { GraphExplorer } from "./GraphExplorer";
@ -69,7 +69,7 @@ describe("Process Gremlin vertex", () => {
describe("getLimitedArrayString()", () => { describe("getLimitedArrayString()", () => {
const expectedEmptyResult = { result: "", consumedCount: 0 }; const expectedEmptyResult = { result: "", consumedCount: 0 };
it("should handle null array", () => { it("should handle null array", () => {
expect(GraphUtil.getLimitedArrayString(null, 10)).toEqual(expectedEmptyResult); expect(GraphUtil.getLimitedArrayString(undefined, 10)).toEqual(expectedEmptyResult);
}); });
it("should handle empty array", () => { it("should handle empty array", () => {

View File

@ -7,8 +7,13 @@ interface JoinArrayMaxCharOutput {
consumedCount: number; // Number of items consumed consumedCount: number; // Number of items consumed
} }
export class GraphUtil { interface EdgePropertyType {
public static getNeighborTitle(neighbor: NeighborVertexBasicInfo): string { id: string;
outV?: string;
inV?: string;
}
export function getNeighborTitle(neighbor: NeighborVertexBasicInfo): string {
return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`; return `edge id: ${neighbor.edgeId}, vertex id: ${neighbor.id}`;
} }
@ -18,17 +23,17 @@ export class GraphUtil {
* @param graphData * @param graphData
* @param newNodes (optional) object describing new nodes encountered * @param newNodes (optional) object describing new nodes encountered
*/ */
public static createEdgesfromNode( export function createEdgesfromNode(
vertex: GraphData.GremlinVertex, vertex: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>, graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>,
newNodes?: { [id: string]: boolean } newNodes?: { [id: string]: boolean }
): void { ): void {
if (vertex.hasOwnProperty("outE")) { if (Object.prototype.hasOwnProperty.call(vertex, "outE")) {
let outE = vertex.outE; const outE = vertex.outE;
for (var label in outE) { for (const label in outE) {
$.each(outE[label], (index: number, edge: any) => { $.each(outE[label], (index: number, edge: EdgePropertyType) => {
// We create our own edge. No need to fetch // We create our own edge. No need to fetch
let e = { const e = {
id: edge.id, id: edge.id,
label: label, label: label,
inV: edge.inV, inV: edge.inV,
@ -42,12 +47,12 @@ export class GraphUtil {
}); });
} }
} }
if (vertex.hasOwnProperty("inE")) { if (Object.prototype.hasOwnProperty.call(vertex, "inE")) {
let inE = vertex.inE; const inE = vertex.inE;
for (var label in inE) { for (const label in inE) {
$.each(inE[label], (index: number, edge: any) => { $.each(inE[label], (index: number, edge: EdgePropertyType) => {
// We create our own edge. No need to fetch // We create our own edge. No need to fetch
let e = { const e = {
id: edge.id, id: edge.id,
label: label, label: label,
inV: vertex.id, inV: vertex.id,
@ -70,7 +75,7 @@ export class GraphUtil {
* @param maxSize * @param maxSize
* @return * @return
*/ */
public static getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput { export function getLimitedArrayString(array: string[], maxSize: number): JoinArrayMaxCharOutput {
if (!array || array.length === 0 || array[0].length + 2 > maxSize) { if (!array || array.length === 0 || array[0].length + 2 > maxSize) {
return { result: "", consumedCount: 0 }; return { result: "", consumedCount: 0 };
} }
@ -93,7 +98,7 @@ export class GraphUtil {
}; };
} }
public static createFetchEdgePairQuery( export function createFetchEdgePairQuery(
outE: boolean, outE: boolean,
pkid: string, pkid: string,
excludedEdgeIds: string[], excludedEdgeIds: string[],
@ -104,8 +109,8 @@ export class GraphUtil {
let gremlinQuery: string; let gremlinQuery: string;
if (excludedEdgeIds.length > 0) { if (excludedEdgeIds.length > 0) {
// build a string up to max char // build a string up to max char
const joined = GraphUtil.getLimitedArrayString(excludedEdgeIds, withoutStepArgMaxLenght); const joined = getLimitedArrayString(excludedEdgeIds, withoutStepArgMaxLenght);
const hasWithoutStep = !!joined.result ? `.has(id, without(${joined.result}))` : ""; const hasWithoutStep = joined.result ? `.has(id, without(${joined.result}))` : "";
if (joined.consumedCount === excludedEdgeIds.length) { if (joined.consumedCount === excludedEdgeIds.length) {
gremlinQuery = `g.V(${pkid}).${outE ? "outE" : "inE"}()${hasWithoutStep}.limit(${pageSize}).as('e').${ gremlinQuery = `g.V(${pkid}).${outE ? "outE" : "inE"}()${hasWithoutStep}.limit(${pageSize}).as('e').${
@ -128,7 +133,7 @@ export class GraphUtil {
/** /**
* Trim graph * Trim graph
*/ */
public static trimGraph( export function trimGraph(
currentRoot: GraphData.GremlinVertex, currentRoot: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) { ) {
@ -141,14 +146,14 @@ export class GraphUtil {
}); });
} }
public static addRootChildToGraph( export function addRootChildToGraph(
root: GraphData.GremlinVertex, root: GraphData.GremlinVertex,
child: GraphData.GremlinVertex, child: GraphData.GremlinVertex,
graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge> graphData: GraphData.GraphData<GraphData.GremlinVertex, GraphData.GremlinEdge>
) { ) {
child._ancestorsId = (root._ancestorsId || []).concat([root.id]); child._ancestorsId = (root._ancestorsId || []).concat([root.id]);
graphData.addVertex(child); graphData.addVertex(child);
GraphUtil.createEdgesfromNode(child, graphData); createEdgesfromNode(child, graphData);
graphData.addNeighborInfo(child); graphData.addNeighborInfo(child);
} }
@ -156,23 +161,23 @@ export class GraphUtil {
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \"" for now.
* @param value * @param value
*/ */
public static escapeDoubleQuotes(value: string): string { export function escapeDoubleQuotes(value: string): string {
return value == null ? value : value.replace(/"/g, '\\"'); return value === undefined ? value : value.replace(/"/g, '\\"');
} }
/** /**
* Surround with double-quotes if val is a string. * Surround with double-quotes if val is a string.
* @param val * @param val
*/ */
public static getQuotedPropValue(ip: ViewModels.InputPropertyValue): string { export function getQuotedPropValue(ip: ViewModels.InputPropertyValue): string {
switch (ip.type) { switch (ip.type) {
case "number": case "number":
case "boolean": case "boolean":
return `${ip.value}`; return `${ip.value}`;
case "null": case "null":
return null; return undefined;
default: default:
return `"${GraphUtil.escapeDoubleQuotes(ip.value as string)}"`; return `"${escapeDoubleQuotes(ip.value as string)}"`;
} }
} }
@ -180,7 +185,6 @@ export class GraphUtil {
* TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now. * TODO Perform minimal substitution to prevent breaking gremlin query and allow \' for now.
* @param value * @param value
*/ */
public static escapeSingleQuotes(value: string): string { export function escapeSingleQuotes(value: string): string {
return value == null ? value : value.replace(/'/g, "\\'"); return value === undefined ? value : value.replace(/'/g, "\\'");
}
} }

View File

@ -5,7 +5,7 @@
import * as React from "react"; import * as React from "react";
import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer"; import { GraphHighlightedNodeData, NeighborVertexBasicInfo } from "./GraphExplorer";
import { GraphUtil } from "./GraphUtil"; import * as GraphUtil from "./GraphUtil";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
export interface ReadOnlyNeighborsComponentProps { export interface ReadOnlyNeighborsComponentProps {

View File

@ -1,5 +1,4 @@
import * as CommandBarUtil from "./CommandBarUtil"; import * as CommandBarUtil from "./CommandBarUtil";
import * as ViewModels from "../../../Contracts/ViewModels";
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar"; import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
@ -26,7 +25,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], backgroundColor); const converteds = CommandBarUtil.convertButton([btn], backgroundColor);
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(!converted.split); expect(converted.split).toBe(undefined);
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc); expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt); expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
expect(converted.text).toEqual(btn.commandButtonLabel); expect(converted.text).toEqual(btn.commandButtonLabel);
@ -50,7 +49,7 @@ describe("CommandBarUtil tests", () => {
const converteds = CommandBarUtil.convertButton([btn], "backgroundColor"); const converteds = CommandBarUtil.convertButton([btn], "backgroundColor");
expect(converteds.length).toBe(1); expect(converteds.length).toBe(1);
const converted = converteds[0]; const converted = converteds[0];
expect(converted.split); expect(converted.split).toBe(true);
expect(converted.subMenuProps.items.length).toBe(btn.children.length); expect(converted.subMenuProps.items.length).toBe(btn.children.length);
for (let i = 0; i < converted.subMenuProps.items.length; i++) { for (let i = 0; i < converted.subMenuProps.items.length; i++) {
expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel); expect(converted.subMenuProps.items[i].text).toEqual(btn.children[i].commandButtonLabel);
@ -64,7 +63,6 @@ describe("CommandBarUtil tests", () => {
} }
const converteds = CommandBarUtil.convertButton(btns, "backgroundColor"); const converteds = CommandBarUtil.convertButton(btns, "backgroundColor");
const keys = converteds.map((btn: ICommandBarItemProps) => btn.key);
const uniqueKeys = converteds const uniqueKeys = converteds
.map((btn: ICommandBarItemProps) => btn.key) .map((btn: ICommandBarItemProps) => btn.key)
.filter((value: string, index: number, self: string[]) => self.indexOf(value) === index); .filter((value: string, index: number, self: string[]) => self.indexOf(value) === index);
@ -75,7 +73,7 @@ describe("CommandBarUtil tests", () => {
const btn = createButton(); const btn = createButton();
const backgroundColor = "backgroundColor"; const backgroundColor = "backgroundColor";
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0]; let converted = CommandBarUtil.convertButton([btn], backgroundColor)[0];
expect(converted.text).toEqual(btn.tooltipText); expect(converted.text).toEqual(btn.tooltipText);

View File

@ -17,7 +17,7 @@ export class ControlBarComponent extends React.Component<ControlBarComponentProp
return commandButtonOptions.map( return commandButtonOptions.map(
(btn: CommandButtonComponentProps, index: number): JSX.Element => { (btn: CommandButtonComponentProps, index: number): JSX.Element => {
// Remove label // Remove label
btn.commandButtonLabel = null; btn.commandButtonLabel = undefined;
return CommandButtonComponent.renderButton(btn, `${index}`); return CommandButtonComponent.renderButton(btn, `${index}`);
} }
); );

View File

@ -1,10 +1,5 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility"; import { StorageKey, LocalStorageUtility } from "../../Shared/StorageUtility";
import CollectionIcon from "../../../images/tree-collection.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import Explorer from "../Explorer";
export enum Type { export enum Type {
OpenCollection, OpenCollection,
OpenNotebook, OpenNotebook,
@ -36,11 +31,11 @@ interface StoredData {
/** /**
* Stores most recent activity * Stores most recent activity
*/ */
export class MostRecentActivity { class MostRecentActivity {
private static readonly schemaVersion: string = "1"; private static readonly schemaVersion: string = "1";
private static itemsMaxNumber: number = 5; private static itemsMaxNumber: number = 5;
private storedData: StoredData; private storedData: StoredData;
constructor(private container: Explorer) { constructor() {
// Retrieve from local storage // Retrieve from local storage
if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) { if (LocalStorageUtility.hasItem(StorageKey.MostRecentActivity)) {
const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity); const rawData = LocalStorageUtility.getEntryString(StorageKey.MostRecentActivity);
@ -121,42 +116,6 @@ export class MostRecentActivity {
this.saveToLocalStorage(); this.saveToLocalStorage();
} }
public onItemClicked(item: Item) {
switch (item.type) {
case Type.OpenCollection: {
const openCollectionitem = item.data as OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
if (collection) {
collection.openTab();
}
break;
}
case Type.OpenNotebook: {
const openNotebookItem = item.data as OpenNotebookItem;
const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path);
notebookItem && this.container.openNotebook(notebookItem);
break;
}
default:
console.error("Unknown item type", item);
break;
}
}
public static getItemIcon(item: Item): string {
switch (item.type) {
case Type.OpenCollection:
return CollectionIcon;
case Type.OpenNotebook:
return NotebookIcon;
default:
return null;
}
}
/** /**
* Find items by doing strict comparison and remove from array if duplicate is found * Find items by doing strict comparison and remove from array if duplicate is found
* @param item * @param item
@ -203,3 +162,5 @@ export class MostRecentActivity {
} }
} }
} }
export const mostRecentActivity = new MostRecentActivity();

View File

@ -3,8 +3,7 @@ import { NotebookContentRecordProps, selectors } from "@nteract/core";
/** /**
* A bunch of utilities to interact with nteract * A bunch of utilities to interact with nteract
*/ */
export default class NTeractUtil { export function getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
public static getCurrentCellType(content: NotebookContentRecordProps): "markdown" | "code" | "raw" | undefined {
if (!content) { if (!content) {
return undefined; return undefined;
} }
@ -19,4 +18,3 @@ export default class NTeractUtil {
return undefined; return undefined;
} }
}

View File

@ -29,7 +29,7 @@ import "@nteract/styles/global-variables.css";
import "react-table/react-table.css"; import "react-table/react-table.css";
import * as CdbActions from "./actions"; import * as CdbActions from "./actions";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
export interface NotebookComponentBootstrapperOptions { export interface NotebookComponentBootstrapperOptions {
notebookClient: NotebookClientV2; notebookClient: NotebookClientV2;

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import { AppState, ContentRef, selectors } from "@nteract/core"; import { AppState, ContentRef, selectors } from "@nteract/core";
import { connect } from "react-redux"; import { connect } from "react-redux";
import NteractUtil from "../NTeractUtil"; import * as NteractUtil from "../NTeractUtil";
interface VirtualCommandBarComponentProps { interface VirtualCommandBarComponentProps {
kernelSpecName: string; kernelSpecName: string;

View File

@ -1,4 +1,4 @@
import { StringUtils } from "../../../../../Utils/StringUtils"; import * as StringUtils from "../../../../../Utils/StringUtils";
import { actions, AppState, ContentRef, selectors } from "@nteract/core"; import { actions, AppState, ContentRef, selectors } from "@nteract/core";
import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor"; import { IMonacoProps as MonacoEditorProps } from "@nteract/monaco-editor";
import * as React from "react"; import * as React from "react";

View File

@ -1,6 +1,6 @@
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../Contracts/DataModels";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import { FileSystemUtil } from "./FileSystemUtil"; import { FileSystemUtil } from "./FileSystemUtil";
import { NotebookUtil } from "./NotebookUtil"; import { NotebookUtil } from "./NotebookUtil";

View File

@ -1,7 +1,7 @@
import path from "path"; import path from "path";
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable"; import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils"; import * as StringUtils from "../../Utils/StringUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
// Must match rx-jupyter' FileType // Must match rx-jupyter' FileType

View File

@ -214,7 +214,6 @@
maxAutoPilotThroughputSet: sharedAutoPilotThroughput, maxAutoPilotThroughputSet: sharedAutoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >
@ -435,7 +434,6 @@
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFixedStorageSelected(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

View File

@ -749,13 +749,17 @@ export default class AddCollectionPane extends ContextualPaneBase {
return undefined; return undefined;
} }
if (this.isAutoPilotSelected()) { // return undefined if autopilot is selected for the new database/collection
return undefined; if (this.databaseCreateNew()) {
} // database is shared and autopilot is sleected for the database
if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) { if (this.databaseCreateNewShared() && this.isSharedAutoPilotSelected()) {
return undefined; return undefined;
} }
// database is not shared and autopilot is selected for the collection
if (!this.databaseCreateNewShared() && this.isAutoPilotSelected()) {
return undefined;
}
}
return this._getThroughput(); return this._getThroughput();
} }

View File

@ -149,7 +149,6 @@
maxAutoPilotThroughputSet: maxAutoPilotThroughputSet, maxAutoPilotThroughputSet: maxAutoPilotThroughputSet,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
showAutoPilot: !isFreeTierAccount(),
freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip freeTierExceedThroughputTooltip: freeTierExceedThroughputTooltip
}" }"
> >

View File

@ -166,7 +166,6 @@
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,
canExceedMaximumValue: canExceedMaximumValue, canExceedMaximumValue: canExceedMaximumValue,
costsVisible: costsVisible, costsVisible: costsVisible,
showAutoPilot: !isFreeTierAccount()
}" }"
> >
</throughput-input-autopilot-v3> </throughput-input-autopilot-v3>

View File

@ -6,7 +6,7 @@ import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
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 * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils"; import * as JunoUtils from "../../Utils/JunoUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent"; import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent"; import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter"; import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";

View File

@ -5,7 +5,7 @@ import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsol
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility"; import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { StringUtility } from "../../Shared/StringUtility"; import * as StringUtility from "../../Shared/StringUtility";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
export class SettingsPane extends ContextualPaneBase { export class SettingsPane extends ContextualPaneBase {

View File

@ -18,6 +18,8 @@ import { DataSamplesUtil } from "../DataSamples/DataSamplesUtil";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { userContext } from "../../UserContext"; import { userContext } from "../../UserContext";
import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher"; import { FeaturePanelLauncher } from "../Controls/FeaturePanel/FeaturePanelLauncher";
import CollectionIcon from "../../../images/tree-collection.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
export interface SplashScreenItem { export interface SplashScreenItem {
iconSrc: string; iconSrc: string;
@ -39,21 +41,34 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability"; private static readonly failoverUrl = "https://docs.microsoft.com/azure/cosmos-db/high-availability";
private readonly container: Explorer; private readonly container: Explorer;
private subscriptions: Array<{ dispose: () => void }>;
constructor(props: SplashScreenProps) { constructor(props: SplashScreenProps) {
super(props); super(props);
this.container = props.explorer; this.container = props.explorer;
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})); this.subscriptions = [];
this.container.selectedNode.subscribe(() => this.setState({}));
this.container.isNotebookEnabled.subscribe(() => this.setState({}));
} }
public shouldComponentUpdate() { public shouldComponentUpdate() {
return this.container.tabsManager.openedTabs.length === 0; return this.container.tabsManager.openedTabs.length === 0;
} }
public componentWillUnmount() {
while (this.subscriptions.length) {
this.subscriptions.pop().dispose();
}
}
public componentDidMount() {
this.subscriptions.push(
this.container.tabsManager.openedTabs.subscribe(() => this.setState({})),
this.container.selectedNode.subscribe(() => this.setState({})),
this.container.isNotebookEnabled.subscribe(() => this.setState({}))
);
}
private clearMostRecent = (): void => { private clearMostRecent = (): void => {
this.container.mostRecentActivity.clear(userContext.databaseAccount?.id); MostRecentActivity.mostRecentActivity.clear(userContext.databaseAccount?.id);
this.setState({}); this.setState({});
}; };
@ -202,6 +217,42 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
return heroes; return heroes;
} }
private getItemIcon(item: MostRecentActivity.Item): string {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection:
return CollectionIcon;
case MostRecentActivity.Type.OpenNotebook:
return NotebookIcon;
default:
return null;
}
}
private onItemClicked(item: MostRecentActivity.Item) {
switch (item.type) {
case MostRecentActivity.Type.OpenCollection: {
const openCollectionitem = item.data as MostRecentActivity.OpenCollectionItem;
const collection = this.container.findCollection(
openCollectionitem.databaseId,
openCollectionitem.collectionId
);
if (collection) {
collection.openTab();
}
break;
}
case MostRecentActivity.Type.OpenNotebook: {
const openNotebookItem = item.data as MostRecentActivity.OpenNotebookItem;
const notebookItem = this.container.createNotebookContentItemFile(openNotebookItem.name, openNotebookItem.path);
notebookItem && this.container.openNotebook(notebookItem);
break;
}
default:
console.error("Unknown item type", item);
break;
}
}
private createCommonTaskItems(): SplashScreenItem[] { private createCommonTaskItems(): SplashScreenItem[] {
const items: SplashScreenItem[] = []; const items: SplashScreenItem[] = [];
@ -292,12 +343,12 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
} }
private createRecentItems(): SplashScreenItem[] { private createRecentItems(): SplashScreenItem[] {
return this.container.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({ return MostRecentActivity.mostRecentActivity.getItems(userContext.databaseAccount?.id).map((item) => ({
iconSrc: MostRecentActivity.MostRecentActivity.getItemIcon(item), iconSrc: this.getItemIcon(item),
title: item.title, title: item.title,
description: item.description, description: item.description,
info: SplashScreen.getInfo(item), info: SplashScreen.getInfo(item),
onClick: () => this.container.mostRecentActivity.onItemClicked(item), onClick: () => this.onItemClicked(item),
})); }));
} }

View File

@ -37,23 +37,6 @@ export function containItems<T>(items: T[]): boolean {
return items && items.length > 0; return items && items.length > 0;
} }
// export function setTargetIcon(idToIconHandlerMap: CloudHub.Common.IToolbarElementIdIconMap, $sourceElement: JQuery, toIconState: IconState): void {
// if (idToIconHandlerMap) {
// var iconId: string = $sourceElement.attr("id");
// var iconHandler = idToIconHandlerMap[iconId];
// switch (toIconState) {
// case IconState.default:
// iconHandler.observable(iconHandler.default);
// break;
// case IconState.hoverState:
// iconHandler.observable(iconHandler.hoverState);
// break;
// default:
// window.console.log("error");
// }
// }
// }
export function addCssClass($sourceElement: JQuery, cssClassName: string): void { export function addCssClass($sourceElement: JQuery, cssClassName: string): void {
if (!$sourceElement.hasClass(cssClassName)) { if (!$sourceElement.hasClass(cssClassName)) {
$sourceElement.addClass(cssClassName); $sourceElement.addClass(cssClassName);
@ -78,8 +61,9 @@ export function getPropertyIntersectionFromTableEntities(
entities: Entities.ITableEntity[], entities: Entities.ITableEntity[],
isCassandraApi: boolean isCassandraApi: boolean
): string[] { ): string[] {
var headerUnion: string[] = []; const headerUnion: string[] = [];
entities && entities &&
// eslint-disable-next-line @typescript-eslint/no-explicit-any
entities.forEach((row: any) => { entities.forEach((row: any) => {
const keys = Object.keys(row); const keys = Object.keys(row);
keys && keys &&

View File

@ -2,26 +2,26 @@ const epochTicks = 621355968000000000;
const ticksPerMillisecond = 10000; const ticksPerMillisecond = 10000;
export function getLocalDateTime(dateTime: string): string { export function getLocalDateTime(dateTime: string): string {
var dateTimeObject: Date = new Date(dateTime); const dateTimeObject: Date = new Date(dateTime);
var year: number = dateTimeObject.getFullYear(); const year: number = dateTimeObject.getFullYear();
var month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11 const month: string = ensureDoubleDigits(dateTimeObject.getMonth() + 1); // Month ranges from 0 to 11
var day: string = ensureDoubleDigits(dateTimeObject.getDate()); const day: string = ensureDoubleDigits(dateTimeObject.getDate());
var hours: string = ensureDoubleDigits(dateTimeObject.getHours()); const hours: string = ensureDoubleDigits(dateTimeObject.getHours());
var minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes()); const minutes: string = ensureDoubleDigits(dateTimeObject.getMinutes());
var seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds()); const seconds: string = ensureDoubleDigits(dateTimeObject.getSeconds());
var milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds()); const milliseconds: string = ensureTripleDigits(dateTimeObject.getMilliseconds());
var localDateTime: string = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`; const localDateTime = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${milliseconds}`;
return localDateTime; return localDateTime;
} }
export function getUTCDateTime(dateTime: string): string { export function getUTCDateTime(dateTime: string): string {
var dateTimeObject: Date = new Date(dateTime); const dateTimeObject = new Date(dateTime);
return dateTimeObject.toISOString(); return dateTimeObject.toISOString();
} }
export function ensureDoubleDigits(num: number): string { export function ensureDoubleDigits(num: number): string {
var doubleDigitsString: string = num.toString(); let doubleDigitsString: string = num.toString();
if (num < 10) { if (num < 10) {
doubleDigitsString = `0${doubleDigitsString}`; doubleDigitsString = `0${doubleDigitsString}`;
} else if (num > 99) { } else if (num > 99) {
@ -31,7 +31,7 @@ export function ensureDoubleDigits(num: number): string {
} }
export function ensureTripleDigits(num: number): string { export function ensureTripleDigits(num: number): string {
var tripleDigitsString: string = num.toString(); let tripleDigitsString: string = num.toString();
if (num < 10) { if (num < 10) {
tripleDigitsString = `00${tripleDigitsString}`; tripleDigitsString = `00${tripleDigitsString}`;
} else if (num < 100) { } else if (num < 100) {
@ -51,17 +51,17 @@ export function convertJSDateToUnix(dateTime: string): number {
} }
export function convertTicksToJSDate(ticks: string): Date { export function convertTicksToJSDate(ticks: string): Date {
var ticksJSBased = Number(ticks) - epochTicks; const ticksJSBased = Number(ticks) - epochTicks;
var timeInMillisecond = ticksJSBased / ticksPerMillisecond; const timeInMillisecond = ticksJSBased / ticksPerMillisecond;
return new Date(timeInMillisecond); return new Date(timeInMillisecond);
} }
export function convertJSDateToTicksWithPadding(dateTime: string): string { export function convertJSDateToTicksWithPadding(dateTime: string): string {
var ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond; const ticks = epochTicks + new Date(dateTime).getTime() * ticksPerMillisecond;
return padDateTicksWithZeros(ticks.toString()); return padDateTicksWithZeros(ticks.toString());
} }
function padDateTicksWithZeros(value: string): string { function padDateTicksWithZeros(value: string): string {
var s = "0000000000000000000" + value; const s = "0000000000000000000" + value;
return s.substr(s.length - 20); return s.substr(s.length - 20);
} }

View File

@ -53,7 +53,6 @@
throughputAutoPilotRadioId: throughputAutoPilotRadioId, throughputAutoPilotRadioId: throughputAutoPilotRadioId,
throughputProvisionedRadioId: throughputProvisionedRadioId, throughputProvisionedRadioId: throughputProvisionedRadioId,
throughputModeRadioName: throughputModeRadioName, throughputModeRadioName: throughputModeRadioName,
showAutoPilot: userCanChangeProvisioningTypes,
isAutoPilotSelected: isAutoPilotSelected, isAutoPilotSelected: isAutoPilotSelected,
maxAutoPilotThroughputSet: autoPilotThroughput, maxAutoPilotThroughputSet: autoPilotThroughput,
autoPilotUsageCost: autoPilotUsageCost, autoPilotUsageCost: autoPilotUsageCost,

View File

@ -1,23 +1,23 @@
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as Constants from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ko from "knockout"; import * as ko from "knockout";
import * as PricingUtils from "../../Utils/PricingUtils";
import * as SharedConstants from "../../Shared/Constants";
import * as ViewModels from "../../Contracts/ViewModels";
import DiscardIcon from "../../../images/discard.svg";
import editable from "../../Common/EditableUtility";
import Q from "q"; import Q from "q";
import DiscardIcon from "../../../images/discard.svg";
import SaveIcon from "../../../images/save-cosmos.svg"; import SaveIcon from "../../../images/save-cosmos.svg";
import TabsBase from "./TabsBase"; import * as Constants from "../../Common/Constants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { RequestOptions } from "@azure/cosmos/dist-esm";
import Explorer from "../Explorer";
import { updateOffer } from "../../Common/dataAccess/updateOffer"; import { updateOffer } from "../../Common/dataAccess/updateOffer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import editable from "../../Common/EditableUtility";
import { configContext, Platform } from "../../ConfigContext";
import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils"; import { getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { configContext, Platform } from "../../ConfigContext";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import * as SharedConstants from "../../Shared/Constants";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as AutoPilotUtils from "../../Utils/AutoPilotUtils";
import * as PricingUtils from "../../Utils/PricingUtils";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
import Explorer from "../Explorer";
import TabsBase from "./TabsBase";
const updateThroughputBeyondLimitWarningMessage: string = ` const updateThroughputBeyondLimitWarningMessage: string = `
You are about to request an increase in throughput beyond the pre-allocated capacity. You are about to request an increase in throughput beyond the pre-allocated capacity.
@ -73,7 +73,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
public shouldShowStatusBar: ko.Computed<boolean>; public shouldShowStatusBar: ko.Computed<boolean>;
public throughputTitle: ko.PureComputed<string>; public throughputTitle: ko.PureComputed<string>;
public throughputAriaLabel: ko.PureComputed<string>; public throughputAriaLabel: ko.PureComputed<string>;
public userCanChangeProvisioningTypes: ko.Observable<boolean>;
public autoPilotUsageCost: ko.PureComputed<string>; public autoPilotUsageCost: ko.PureComputed<string>;
public warningMessage: ko.Computed<string>; public warningMessage: ko.Computed<string>;
public canExceedMaximumValue: ko.PureComputed<boolean>; public canExceedMaximumValue: ko.PureComputed<boolean>;
@ -106,7 +105,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this._wasAutopilotOriginallySet = ko.observable(false); this._wasAutopilotOriginallySet = ko.observable(false);
this.isAutoPilotSelected = editable.observable(false); this.isAutoPilotSelected = editable.observable(false);
this.autoPilotThroughput = editable.observable<number>(); this.autoPilotThroughput = editable.observable<number>();
this.userCanChangeProvisioningTypes = ko.observable(true);
const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput; const autoscaleMaxThroughput = this.database?.offer()?.autoscaleMaxThroughput;
if (autoscaleMaxThroughput) { if (autoscaleMaxThroughput) {
@ -118,9 +116,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
} }
this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => { this._hasProvisioningTypeChanged = ko.pureComputed<boolean>(() => {
if (!this.userCanChangeProvisioningTypes()) {
return false;
}
if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) { if (this._wasAutopilotOriginallySet() !== this.isAutoPilotSelected()) {
return true; return true;
} }
@ -136,7 +131,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
}); });
this.requestUnitsUsageCost = ko.pureComputed(() => { this.requestUnitsUsageCost = ko.pureComputed(() => {
const account = this.container.databaseAccount(); const account = userContext.databaseAccount;
if (!account) { if (!account) {
return ""; return "";
} }
@ -362,7 +357,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isTemplateReady = ko.observable<boolean>(false); this.isTemplateReady = ko.observable<boolean>(false);
this.isFreeTierAccount = ko.computed<boolean>(() => { this.isFreeTierAccount = ko.computed<boolean>(() => {
const databaseAccount = this.container?.databaseAccount(); const databaseAccount = userContext.databaseAccount;
return databaseAccount?.properties?.enableFreeTier; return databaseAccount?.properties?.enableFreeTier;
}); });
@ -448,7 +443,6 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput)); this.isAutoPilotSelected.setBaseline(AutoPilotUtils.isValidAutoPilotThroughput(offer.autoscaleMaxThroughput));
this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput); this.autoPilotThroughput.setBaseline(offer.autoscaleMaxThroughput);
this.throughput.setBaseline(offer.manualThroughput); this.throughput.setBaseline(offer.manualThroughput);
this.userCanChangeProvisioningTypes(true);
} }
protected getTabsButtons(): CommandButtonComponentProps[] { protected getTabsButtons(): CommandButtonComponentProps[] {

View File

@ -24,7 +24,7 @@ import * as CommandBarComponentButtonFactory from "../Menus/CommandBar/CommandBa
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter"; import { NotebookComponentAdapter } from "../Notebook/NotebookComponent/NotebookComponentAdapter";
import { NotebookConfigurationUtils } from "../../Utils/NotebookConfigurationUtils"; import * as NotebookConfigurationUtils from "../../Utils/NotebookConfigurationUtils";
import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2"; import { KernelSpecsDisplay, NotebookClientV2 } from "../Notebook/NotebookClientV2";
import { configContext } from "../../ConfigContext"; import { configContext } from "../../ConfigContext";
import Explorer from "../Explorer"; import Explorer from "../Explorer";

View File

@ -7,7 +7,7 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
import { RouteHandler } from "../../RouteHandlers/RouteHandler"; import { RouteHandler } from "../../RouteHandlers/RouteHandler";
import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel"; import { WaitsForTemplateViewModel } from "../WaitsForTemplateViewModel";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ThemeUtility from "../../Common/ThemeUtility"; import * as ThemeUtility from "../../Common/ThemeUtility";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent"; import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";

View File

@ -179,7 +179,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
onClick: () => { onClick: () => {
collection.openTab(); collection.openTab();
// push to most recent // push to most recent
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, { MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenCollection, type: MostRecentActivity.Type.OpenCollection,
title: collection.id(), title: collection.id(),
description: "Data", description: "Data",
@ -544,7 +544,7 @@ export class ResourceTree extends React.Component<ResourceTreeProps> {
} }
private pushItemToMostRecent(item: NotebookContentItem) { private pushItemToMostRecent(item: NotebookContentItem) {
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, { MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenNotebook, type: MostRecentActivity.Type.OpenNotebook,
title: item.name, title: item.name,
description: "Notebook", description: "Notebook",

View File

@ -13,7 +13,6 @@ const createMockContainer = (): Explorer => {
let mockContainer = {} as Explorer; let mockContainer = {} as Explorer;
mockContainer.resourceTokenCollection = createMockCollection(mockContainer); mockContainer.resourceTokenCollection = createMockCollection(mockContainer);
mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>(); mockContainer.selectedNode = ko.observable<ViewModels.TreeNode>();
mockContainer.mostRecentActivity = new MostRecentActivity.MostRecentActivity(mockContainer);
mockContainer.onUpdateTabsButtons = () => {}; mockContainer.onUpdateTabsButtons = () => {};
return mockContainer; return mockContainer;

View File

@ -44,7 +44,7 @@ export class ResourceTreeAdapterForResourceToken implements ReactAdapter {
onClick: () => { onClick: () => {
collection.onDocumentDBDocumentsClick(); collection.onDocumentDBDocumentsClick();
// push to most recent // push to most recent
this.container.mostRecentActivity.addItem(userContext.databaseAccount?.id, { MostRecentActivity.mostRecentActivity.addItem(userContext.databaseAccount?.id, {
type: MostRecentActivity.Type.OpenCollection, type: MostRecentActivity.Type.OpenCollection,
title: collection.id(), title: collection.id(),
description: "Data", description: "Data",

View File

@ -26,7 +26,7 @@ const onInit = async () => {
const props: GalleryAndNotebookViewerComponentProps = { const props: GalleryAndNotebookViewerComponentProps = {
junoClient: new JunoClient(), junoClient: new JunoClient(),
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples, selectedTab: galleryViewerProps.selectedTab || GalleryTab.PublicGallery,
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed, sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
searchText: galleryViewerProps.searchText, searchText: galleryViewerProps.searchText,
}; };
@ -36,7 +36,7 @@ const onInit = async () => {
<header> <header>
<GalleryHeaderComponent /> <GalleryHeaderComponent />
</header> </header>
<div style={{ marginLeft: 138, marginRight: 138 }}> <div style={{ margin: "auto", width: "85%" }}>
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}> <div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
<Text block> <Text block>
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best

View File

@ -9,9 +9,11 @@
"North Central US": "North Central US", "North Central US": "North Central US",
"West US": "West US", "West US": "West US",
"East US 2": "East US 2", "East US 2": "East US 2",
"ClassInfo": "This is a self serve class", "Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.", "RegionDropdownInfo": "More regions can be added in the future.",
"ValidationError": "Regions and AccountName should not be empty.", "RegionsAndAccountNameValidationError": "Regions and account name should not be empty.",
"DbThroughputValidationError": "Please update throughput for database.",
"DescriptionLabel": "Description",
"DescriptionText": "This class sets collection and database throughput.", "DescriptionText": "This class sets collection and database throughput.",
"DecriptionLinkText": "Click here for more information", "DecriptionLinkText": "Click here for more information",
"Regions": "Regions", "Regions": "Regions",
@ -22,10 +24,17 @@
"Account Name": "Account Name", "Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name", "AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput", "Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable DB level throughput", "Enable DB level throughput": "Enable Database Level Throughput",
"Database Throughput": "Database Throughput", "Database Throughput": "Database Throughput",
"RefreshMessage": "Self Serve Example successfully refreshing", "UpdateInProgressMessage": "Data is being updated",
"SubmissionMessage": "Submitted successfully" "UpdateCompletedMessageTitle":"Update succeeded",
"UpdateCompletedMessageText": "Data updation completed.",
"SubmissionMessageSuccessTitle": "Update started",
"SubmissionMessageForNewRegionText": "Data update started. Region changed.",
"SubmissionMessageForSameRegionText": "Data update started. Region not changed.",
"SubmissionMessageErrorTitle": "Data update failed",
"SubmissionMessageErrorText": "Data update failed because of errors.",
"OnSaveFailureMessage": "Data save operation not currently permitted."
}, },
"SqlX": { "SqlX": {
} }

View File

@ -133,17 +133,8 @@ const App: React.FunctionComponent = () => {
return ( return (
<div className="flexContainer"> <div className="flexContainer">
<div <div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
id="divSelfServe" {/* Main Command Bar - Start */}
className="flexContainer"
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
></div>
<div
id="divExplorer"
data-bind="if: selfServeType() === 'none'"
className="flexContainer hideOverflows"
style={{ display: "none" }}
>
<div data-bind="react: commandBarComponentAdapter" /> <div data-bind="react: commandBarComponentAdapter" />
{/* Collections Tree and Tabs - Begin */} {/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs"> <div className="resourceTreeAndTabs">
@ -282,11 +273,8 @@ const App: React.FunctionComponent = () => {
</div> </div>
</div> </div>
{/* Global loader - Start */} {/* Global loader - Start */}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer"> <div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer"> <div className="splashLoaderContentContainer">
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
<div data-bind="if: selfServeType() === 'none'" style={{ display: "none" }}>
<p className="connectExplorerContent"> <p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" /> <img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p> </p>
@ -298,7 +286,6 @@ const App: React.FunctionComponent = () => {
</p> </p>
</div> </div>
</div> </div>
</div>
{/* Global loader - End */} {/* Global loader - End */}
<PanelContainerComponent <PanelContainerComponent
isOpen={isPanelOpen} isOpen={isPanelOpen}

View File

@ -11,6 +11,7 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils"; import * as GalleryUtils from "../Utils/GalleryUtils";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent"; import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import { FileSystemUtil } from "../Explorer/Notebook/FileSystemUtil"; import { FileSystemUtil } from "../Explorer/Notebook/FileSystemUtil";
import { GalleryTab } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
const onInit = async () => { const onInit = async () => {
initializeIcons(); initializeIcons();
@ -21,7 +22,10 @@ const onInit = async () => {
let onBackClick: () => void; let onBackClick: () => void;
if (galleryViewerProps.selectedTab !== undefined) { if (galleryViewerProps.selectedTab !== undefined) {
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab); backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
onBackClick = () => (window.location.href = `${configContext.hostedExplorerURL}gallery.html`); onBackClick = () =>
(window.location.href = `${configContext.hostedExplorerURL}gallery.html?tab=${
GalleryTab[galleryViewerProps.selectedTab]
}`);
} }
const hideInputs = notebookViewerProps.hideInputs; const hideInputs = notebookViewerProps.hideInputs;

View File

@ -1,4 +1,4 @@
import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput } from "./SelfServeTypes"; import { ChoiceItem, Description, Info, InputType, NumberUiType, SmartUiInput, RefreshParams } from "./SelfServeTypes";
import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils"; import { addPropertyToMap, DecoratorProperties, buildSmartUiDescriptor } from "./SelfServeUtils";
type ValueOf<T> = T[keyof T]; type ValueOf<T> = T[keyof T];
@ -33,7 +33,9 @@ export interface ChoiceInputOptions extends InputOptionsBase {
} }
export interface DescriptionDisplayOptions { export interface DescriptionDisplayOptions {
labelTKey?: string;
description?: (() => Promise<Description>) | Description; description?: (() => Promise<Description>) | Description;
isDynamicDescription?: boolean;
} }
type InputOptions = type InputOptions =
@ -56,7 +58,7 @@ const isChoiceInputOptions = (inputOptions: InputOptions): inputOptions is Choic
}; };
const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => { const isDescriptionDisplayOptions = (inputOptions: InputOptions): inputOptions is DescriptionDisplayOptions => {
return "description" in inputOptions; return "description" in inputOptions || "isDynamicDescription" in inputOptions;
}; };
const addToMap = (...decorators: Decorator[]): PropertyDecorator => { const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
@ -80,7 +82,11 @@ const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
}; };
export const OnChange = ( export const OnChange = (
onChange: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput> onChange: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>
): PropertyDecorator => { ): PropertyDecorator => {
return addToMap({ name: "onChange", value: onChange }); return addToMap({ name: "onChange", value: onChange });
}; };
@ -111,7 +117,11 @@ export const Values = (inputOptions: InputOptions): PropertyDecorator => {
{ name: "choices", value: inputOptions.choices } { name: "choices", value: inputOptions.choices }
); );
} else if (isDescriptionDisplayOptions(inputOptions)) { } else if (isDescriptionDisplayOptions(inputOptions)) {
return addToMap({ name: "description", value: inputOptions.description }); return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey },
{ name: "description", value: inputOptions.description },
{ name: "isDynamicDescription", value: inputOptions.isDynamicDescription }
);
} else { } else {
return addToMap( return addToMap(
{ name: "labelTKey", value: inputOptions.labelTKey }, { name: "labelTKey", value: inputOptions.labelTKey },
@ -126,8 +136,8 @@ export const IsDisplayable = (): ClassDecorator => {
}; };
}; };
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => { export const RefreshOptions = (refreshParams: RefreshParams): ClassDecorator => {
return (target) => { return (target) => {
addPropertyToMap(target.prototype, "root", target.name, "info", info); addPropertyToMap(target.prototype, "root", target.name, "refreshParams", refreshParams);
}; };
}; };

View File

@ -64,13 +64,20 @@ export const initialize = async (): Promise<InitializeResponse> => {
}; };
export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => { export const onRefreshSelfServeExample = async (): Promise<RefreshResult> => {
const refreshCountString = SessionStorageUtility.getEntry("refreshCount");
const refreshCount = refreshCountString ? parseInt(refreshCountString) : 0;
const subscriptionId = userContext.subscriptionId; const subscriptionId = userContext.subscriptionId;
const resourceGroup = userContext.resourceGroup; const resourceGroup = userContext.resourceGroup;
const databaseAccountName = userContext.databaseAccount.name; const databaseAccountName = userContext.databaseAccount.name;
const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName); const databaseAccountGetResults = await get(subscriptionId, resourceGroup, databaseAccountName);
const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded"; const isUpdateInProgress = databaseAccountGetResults.properties.provisioningState !== "Succeeded";
const progressToBeSent = refreshCount % 5 === 0 ? isUpdateInProgress : true;
SessionStorageUtility.setEntry("refreshCount", (refreshCount + 1).toString());
return { return {
isUpdateInProgress: isUpdateInProgress, isUpdateInProgress: progressToBeSent,
notificationMessage: "RefreshMessage", updateInProgressMessageTKey: "UpdateInProgressMessage",
}; };
}; };

View File

@ -1,13 +1,14 @@
import { PropertyInfo, OnChange, Values, IsDisplayable, ClassInfo } from "../Decorators"; import { PropertyInfo, OnChange, Values, IsDisplayable, RefreshOptions } from "../Decorators";
import { import {
ChoiceItem, ChoiceItem,
Description,
DescriptionType,
Info, Info,
InputType, InputType,
NumberUiType, NumberUiType,
OnSaveResult,
RefreshResult, RefreshResult,
SelfServeBaseClass, SelfServeBaseClass,
SelfServeNotification,
SelfServeNotificationType,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { import {
@ -27,16 +28,19 @@ const regionDropdownItems: ChoiceItem[] = [
{ label: "East US 2", key: Regions.EastUS2 }, { label: "East US 2", key: Regions.EastUS2 },
]; ];
const selfServeExampleInfo: Info = {
messageTKey: "ClassInfo",
};
const regionDropdownInfo: Info = { const regionDropdownInfo: Info = {
messageTKey: "RegionDropdownInfo", messageTKey: "RegionDropdownInfo",
}; };
const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: InputType): Map<string, SmartUiInput> => { const onRegionsChange = (newValue: InputType, currentState: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
currentState.set("regions", { value: newValue }); currentState.set("regions", { value: newValue });
const currentRegionText = `current region selected is ${newValue}`;
currentState.set("currentRegionText", {
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
hidden: false,
});
const currentEnableLogging = currentState.get("enableLogging"); const currentEnableLogging = currentState.get("enableLogging");
if (newValue === Regions.NorthCentralUS) { if (newValue === Regions.NorthCentralUS) {
currentState.set("enableLogging", { value: false, disabled: true }); currentState.set("enableLogging", { value: false, disabled: true });
@ -47,8 +51,8 @@ const onRegionsChange = (currentState: Map<string, SmartUiInput>, newValue: Inpu
}; };
const onEnableDbLevelThroughputChange = ( const onEnableDbLevelThroughputChange = (
currentState: Map<string, SmartUiInput>, newValue: InputType,
newValue: InputType currentState: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
currentState.set("enableDbLevelThroughput", { value: newValue }); currentState.set("enableDbLevelThroughput", { value: newValue });
const currentDbThroughput = currentState.get("dbThroughput"); const currentDbThroughput = currentState.get("dbThroughput");
@ -57,9 +61,15 @@ const onEnableDbLevelThroughputChange = (
return currentState; return currentState;
}; };
const validate = (currentvalues: Map<string, SmartUiInput>): void => { const validate = (
currentvalues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): void => {
if (currentvalues.get("dbThroughput") === baselineValues.get("dbThroughput")) {
throw new Error("DbThroughputValidationError");
}
if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) { if (!currentvalues.get("regions").value || !currentvalues.get("accountName").value) {
throw new Error("ValidationError"); throw new Error("RegionsAndAccountNameValidationError");
} }
}; };
@ -86,12 +96,12 @@ const validate = (currentvalues: Map<string, SmartUiInput>): void => {
*/ */
@IsDisplayable() @IsDisplayable()
/* /*
@ClassInfo() @RefreshOptions()
- optional - role: Passes the refresh options to be used by the self serve model.
- input: Info | () => Promise<Info> - inputs:
- role: Display an Info bar as the first element of the UI. retryIntervalInMs - The time interval between refresh attempts when an update in ongoing.
*/ */
@ClassInfo(selfServeExampleInfo) @RefreshOptions({ retryIntervalInMs: 2000 })
export default class SelfServeExample extends SelfServeBaseClass { export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onRefresh() onRefresh()
@ -109,18 +119,21 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
onSave() onSave()
- input: (currentValues: Map<string, InputType>) => Promise<void> - input: (currentValues: Map<string, InputType>, baselineValues: ReadonlyMap<string, SmartUiInput>) => Promise<string>
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API - role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
calls here using the data from the different inputs passed as a Map to this callback function. calls here using the data from the different inputs passed as a Map to this callback function.
In this example, the onSave callback simply sets the value for keys corresponding to the field name In this example, the onSave callback simply sets the value for keys corresponding to the field name
in the SessionStorage. in the SessionStorage. It uses the currentValues and baselineValues maps to perform custom validations
- returns: SelfServeNotification - as well.
message: The message to be displayed in the message bar after the onSave is completed
type: The type of message bar to be used (info, warning, error) - returns: The initialize, success and failure messages to be displayed in the Portal Notification blade after the operation is completed.
*/ */
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => { public onSave = async (
validate(currentValues); currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
): Promise<OnSaveResult> => {
validate(currentValues, baselineValues);
const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions]; const regions = Regions[currentValues.get("regions")?.value as keyof typeof Regions];
const enableLogging = currentValues.get("enableLogging")?.value as boolean; const enableLogging = currentValues.get("enableLogging")?.value as boolean;
const accountName = currentValues.get("accountName")?.value as string; const accountName = currentValues.get("accountName")?.value as string;
@ -128,8 +141,48 @@ export default class SelfServeExample extends SelfServeBaseClass {
const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean; const enableDbLevelThroughput = currentValues.get("enableDbLevelThroughput")?.value as boolean;
let dbThroughput = currentValues.get("dbThroughput")?.value as number; let dbThroughput = currentValues.get("dbThroughput")?.value as number;
dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined; dbThroughput = enableDbLevelThroughput ? dbThroughput : undefined;
try {
await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput); await update(regions, enableLogging, accountName, collectionThroughput, dbThroughput);
return { message: "SubmissionMessage", type: SelfServeNotificationType.info }; if (currentValues.get("regions") === baselineValues.get("regions")) {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForSameRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
} else {
return {
operationStatusUrl: undefined,
portalNotification: {
initialize: {
titleTKey: "SubmissionMessageSuccessTitle",
messageTKey: "SubmissionMessageForNewRegionText",
},
success: {
titleTKey: "UpdateCompletedMessageTitle",
messageTKey: "UpdateCompletedMessageText",
},
failure: {
titleTKey: "SubmissionMessageErrorTitle",
messageTKey: "SubmissionMessageErrorText",
},
},
};
}
} catch (error) {
throw new Error("OnSaveFailureMessage");
}
}; };
/* /*
@ -150,6 +203,11 @@ export default class SelfServeExample extends SelfServeBaseClass {
public initialize = async (): Promise<Map<string, SmartUiInput>> => { public initialize = async (): Promise<Map<string, SmartUiInput>> => {
const initializeResponse = await initialize(); const initializeResponse = await initialize();
const defaults = new Map<string, SmartUiInput>(); const defaults = new Map<string, SmartUiInput>();
const currentRegionText = `current region selected is ${initializeResponse.regions}`;
defaults.set("currentRegionText", {
value: { textTKey: currentRegionText, type: DescriptionType.Text } as Description,
hidden: false,
});
defaults.set("regions", { value: initializeResponse.regions }); defaults.set("regions", { value: initializeResponse.regions });
defaults.set("enableLogging", { value: initializeResponse.enableLogging }); defaults.set("enableLogging", { value: initializeResponse.enableLogging });
const accountName = initializeResponse.accountName; const accountName = initializeResponse.accountName;
@ -172,15 +230,24 @@ export default class SelfServeExample extends SelfServeBaseClass {
e) Text (with optional hyperlink) for descriptions e) Text (with optional hyperlink) for descriptions
*/ */
@Values({ @Values({
labelTKey: "DescriptionLabel",
description: { description: {
textTKey: "DescriptionText", textTKey: "DescriptionText",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://aka.ms/cosmos-create-account-portal",
textTKey: "DecriptionLinkText", textTKey: "DecriptionLinkText",
}, },
}, },
}) })
description: string; description: string;
@Values({
labelTKey: "Current Region",
isDynamicDescription: true,
})
currentRegionText: string;
/* /*
@PropertyInfo() @PropertyInfo()
- optional - optional
@ -192,8 +259,8 @@ export default class SelfServeExample extends SelfServeBaseClass {
/* /*
@OnChange() @OnChange()
- optional - optional
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType> - input: (currentValues: Map<string, InputType>, newValue: InputType, baselineValues: ReadonlyMap<string, SmartUiInput>) => Map<string, InputType>
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property, - role: Takes a Map of current values, the newValue for this property and a ReadonlyMap of baselineValues as inputs. This is called when a property,
say prop1, changes its value in the UI. This can be used to say prop1, changes its value in the UI. This can be used to
a) Change the value (and reflect it in the UI) for prop2 based on prop1. a) Change the value (and reflect it in the UI) for prop2 based on prop1.
b) Change the visibility for prop2 in the UI, based on prop1 b) Change the visibility for prop2 in the UI, based on prop1

View File

@ -0,0 +1,16 @@
.selfServeComponentContainer {
text-transform: none;
line-height: 1.28581;
letter-spacing: 0;
font-size: 14px;
font-weight: 400;
color: #182026;
height: 100%;
min-height: 100vh;
width: 100%;
background-color: #FFFFFF;
}
body {
margin: 0;
}

View File

@ -0,0 +1,92 @@
import * as React from "react";
import ReactDOM from "react-dom";
import { sendMessage } from "../Common/MessageHandler";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
import { SelfServeFrameInputs } from "../Contracts/ViewModels";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { configContext, updateConfigContext } from "../ConfigContext";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { updateUserContext } from "../UserContext";
import "./SelfServe.less";
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
initializeIcons();
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
return new SqlX.default().toSelfServeDescriptor();
}
default:
return undefined;
}
};
const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element => {
if (!selfServeDescriptor) {
return <h1>Invalid self serve type!</h1>;
}
return <SelfServeComponent descriptor={selfServeDescriptor} />;
};
const renderSpinner = (): JSX.Element => {
return <Spinner size={SpinnerSize.large}></Spinner>;
};
const handleMessage = async (event: MessageEvent): Promise<void> => {
if (isInvalidParentFrameOrigin(event)) {
return;
}
if (event.data["signature"] !== "pcIframe") {
return;
}
if (typeof event.data !== "object") {
return;
}
const inputs = event.data.data.inputs as SelfServeFrameInputs;
if (!inputs) {
return;
}
const urlSearchParams = new URLSearchParams(window.location.search);
const selfServeTypeText = inputs.selfServeType || urlSearchParams.get("selfServeType");
const selfServeType = SelfServeType[selfServeTypeText?.toLowerCase() as keyof typeof SelfServeType];
if (
!inputs.subscriptionId ||
!inputs.resourceGroup ||
!inputs.databaseAccount ||
!inputs.authorizationToken ||
!inputs.csmEndpoint ||
!selfServeType
) {
return;
}
updateConfigContext({
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken: inputs.authorizationToken,
databaseAccount: inputs.databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
});
const descriptor = await getDescriptor(selfServeType);
ReactDOM.render(renderComponent(descriptor), document.getElementById("selfServeContent"));
};
ReactDOM.render(renderSpinner(), document.getElementById("selfServeContent"));
window.addEventListener("message", handleMessage, false);
sendMessage("ready");

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent"; import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
import { NumberUiType, SelfServeDescriptor, SelfServeNotificationType, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
describe("SelfServeComponent", () => { describe("SelfServeComponent", () => {
const defaultValues = new Map<string, SmartUiInput>([ const defaultValues = new Map<string, SmartUiInput>([
@ -17,13 +17,20 @@ describe("SelfServeComponent", () => {
const initializeMock = jest.fn(async () => new Map(defaultValues)); const initializeMock = jest.fn(async () => new Map(defaultValues));
const onSaveMock = jest.fn(async () => { const onSaveMock = jest.fn(async () => {
return { message: "submitted successfully", type: SelfServeNotificationType.info }; return {
operationStatusUrl: undefined,
} as OnSaveResult;
}); });
const refreshResult = {
isUpdateInProgress: false,
updateInProgressMessageTKey: "refresh performed successfully",
};
const onRefreshMock = jest.fn(async () => { const onRefreshMock = jest.fn(async () => {
return { isUpdateInProgress: false, notificationMessage: "refresh performed successfully" }; return { ...refreshResult };
}); });
const onRefreshIsUpdatingMock = jest.fn(async () => { const onRefreshIsUpdatingMock = jest.fn(async () => {
return { isUpdateInProgress: true, notificationMessage: "refresh performed successfully" }; return { ...refreshResult, isUpdateInProgress: true };
}); });
const exampleData: SelfServeDescriptor = { const exampleData: SelfServeDescriptor = {
@ -136,16 +143,15 @@ describe("SelfServeComponent", () => {
wrapper.update(); wrapper.update();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, updatedValues); isEqual(state.baselineValues, updatedValues);
selfServeComponent.resetBaselineValues(); selfServeComponent.updateBaselineValues();
state = wrapper.state() as SelfServeComponentState; state = wrapper.state() as SelfServeComponentState;
isEqual(state.baselineValues, defaultValues); isEqual(state.baselineValues, defaultValues);
isEqual(state.currentValues, state.baselineValues); isEqual(state.currentValues, state.baselineValues);
// clicking refresh calls onRefresh. If component is not updating, it calls initialize() as well // clicking refresh calls onRefresh.
selfServeComponent.onRefreshClicked(); selfServeComponent.onRefreshClicked();
await new Promise((resolve) => setTimeout(resolve, 0)); await new Promise((resolve) => setTimeout(resolve, 0));
expect(onRefreshMock).toHaveBeenCalledTimes(2); expect(onRefreshMock).toHaveBeenCalledTimes(2);
expect(initializeMock).toHaveBeenCalledTimes(2);
selfServeComponent.onSaveButtonClick(); selfServeComponent.onSaveButtonClick();
expect(onSaveMock).toHaveBeenCalledTimes(1); expect(onSaveMock).toHaveBeenCalledTimes(1);

View File

@ -15,20 +15,45 @@ import {
InputType, InputType,
RefreshResult, RefreshResult,
SelfServeDescriptor, SelfServeDescriptor,
SelfServeNotification,
SmartUiInput, SmartUiInput,
DescriptionDisplay, DescriptionDisplay,
StringInput, StringInput,
NumberInput, NumberInput,
BooleanInput, BooleanInput,
ChoiceInput, ChoiceInput,
SelfServeNotificationType,
} from "./SelfServeTypes"; } from "./SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent"; import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { getMessageBarType } from "./SelfServeUtils";
import { Translation } from "react-i18next"; import { Translation } from "react-i18next";
import { TFunction } from "i18next"; import { TFunction } from "i18next";
import "../i18n"; import "../i18n";
import { sendMessage } from "../Common/MessageHandler";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import promiseRetry, { AbortError } from "p-retry";
interface SelfServeNotification {
message: string;
type: MessageBarType;
isCancellable: boolean;
}
interface PortalNotificationContent {
retryIntervalInMs: number;
operationStatusUrl: string;
portalNotification?: {
initialize: {
title: string;
message: string;
};
success: {
title: string;
message: string;
};
failure: {
title: string;
message: string;
};
};
}
export interface SelfServeComponentProps { export interface SelfServeComponentProps {
descriptor: SelfServeDescriptor; descriptor: SelfServeDescriptor;
@ -39,17 +64,26 @@ export interface SelfServeComponentState {
currentValues: Map<string, SmartUiInput>; currentValues: Map<string, SmartUiInput>;
baselineValues: Map<string, SmartUiInput>; baselineValues: Map<string, SmartUiInput>;
isInitializing: boolean; isInitializing: boolean;
isSaving: boolean;
hasErrors: boolean; hasErrors: boolean;
compileErrorMessage: string; compileErrorMessage: string;
notification: SelfServeNotification;
refreshResult: RefreshResult; refreshResult: RefreshResult;
notification: SelfServeNotification;
} }
export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> { export class SelfServeComponent extends React.Component<SelfServeComponentProps, SelfServeComponentState> {
private static readonly defaultRetryIntervalInMs = 30000;
private smartUiGeneratorClassName: string; private smartUiGeneratorClassName: string;
private retryIntervalInMs: number;
private retryOptions: promiseRetry.Options;
private translationFunction: TFunction;
componentDidMount(): void { componentDidMount(): void {
this.performRefresh(); this.performRefresh().then(() => {
if (this.state.refreshResult?.isUpdateInProgress) {
promiseRetry(() => this.pollRefresh(), this.retryOptions);
}
});
this.initializeSmartUiComponent(); this.initializeSmartUiComponent();
} }
@ -60,12 +94,18 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
currentValues: new Map(), currentValues: new Map(),
baselineValues: new Map(), baselineValues: new Map(),
isInitializing: true, isInitializing: true,
isSaving: false,
hasErrors: false, hasErrors: false,
compileErrorMessage: undefined, compileErrorMessage: undefined,
notification: undefined,
refreshResult: undefined, refreshResult: undefined,
notification: undefined,
}; };
this.smartUiGeneratorClassName = this.props.descriptor.root.id; this.smartUiGeneratorClassName = this.props.descriptor.root.id;
this.retryIntervalInMs = this.props.descriptor.refreshParams?.retryIntervalInMs;
if (!this.retryIntervalInMs) {
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
}
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
} }
private onError = (hasErrors: boolean): void => { private onError = (hasErrors: boolean): void => {
@ -109,7 +149,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.setState({ currentValues, baselineValues }); this.setState({ currentValues, baselineValues });
}; };
public resetBaselineValues = (): void => { public updateBaselineValues = (): void => {
const currentValues = this.state.currentValues; const currentValues = this.state.currentValues;
let baselineValues = this.state.baselineValues; let baselineValues = this.state.baselineValues;
for (const key of currentValues.keys()) { for (const key of currentValues.keys()) {
@ -204,7 +244,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
private onInputChange = (input: AnyDisplay, newValue: InputType) => { private onInputChange = (input: AnyDisplay, newValue: InputType) => {
if (input.onChange) { if (input.onChange) {
const newValues = input.onChange(this.state.currentValues, newValue); const newValues = input.onChange(
newValue,
this.state.currentValues,
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
);
this.setState({ currentValues: newValues }); this.setState({ currentValues: newValues });
} else { } else {
const dataFieldName = input.dataFieldName; const dataFieldName = input.dataFieldName;
@ -215,29 +259,62 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
} }
}; };
public performSave = async (): Promise<void> => {
this.setState({ isSaving: true, notification: undefined });
try {
const onSaveResult = await this.props.descriptor.onSave(
this.state.currentValues,
this.state.baselineValues as ReadonlyMap<string, SmartUiInput>
);
if (onSaveResult.portalNotification) {
const requestInitializedPortalNotification = onSaveResult.portalNotification.initialize;
const requestSucceededPortalNotification = onSaveResult.portalNotification.success;
const requestFailedPortalNotification = onSaveResult.portalNotification.failure;
this.sendNotificationMessage({
retryIntervalInMs: this.retryIntervalInMs,
operationStatusUrl: onSaveResult.operationStatusUrl,
portalNotification: {
initialize: {
title: this.getTranslation(requestInitializedPortalNotification.titleTKey),
message: this.getTranslation(requestInitializedPortalNotification.messageTKey),
},
success: {
title: this.getTranslation(requestSucceededPortalNotification.titleTKey),
message: this.getTranslation(requestSucceededPortalNotification.messageTKey),
},
failure: {
title: this.getTranslation(requestFailedPortalNotification.titleTKey),
message: this.getTranslation(requestFailedPortalNotification.messageTKey),
},
},
});
}
promiseRetry(() => this.pollRefresh(), this.retryOptions);
} catch (error) {
this.setState({
notification: {
type: MessageBarType.error,
isCancellable: true,
message: this.getTranslation(error.message),
},
});
throw error;
} finally {
this.setState({ isSaving: false });
}
await this.onRefreshClicked();
this.updateBaselineValues();
};
public onSaveButtonClick = (): void => { public onSaveButtonClick = (): void => {
const onSavePromise = this.props.descriptor.onSave(this.state.currentValues); this.performSave();
onSavePromise.catch((error) => {
this.setState({
notification: {
message: `${error.message}`,
type: SelfServeNotificationType.error,
},
});
});
onSavePromise.then((notification: SelfServeNotification) => {
this.setState({
notification: {
message: notification.message,
type: notification.type,
},
});
this.resetBaselineValues();
this.onRefreshClicked();
});
}; };
public isDiscardButtonDisabled = (): boolean => { public isDiscardButtonDisabled = (): boolean => {
if (this.state.isSaving) {
return true;
}
for (const key of this.state.currentValues.keys()) { for (const key of this.state.currentValues.keys()) {
const currentValue = JSON.stringify(this.state.currentValues.get(key)); const currentValue = JSON.stringify(this.state.currentValues.get(key));
const baselineValue = JSON.stringify(this.state.baselineValues.get(key)); const baselineValue = JSON.stringify(this.state.baselineValues.get(key));
@ -250,7 +327,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}; };
public isSaveButtonDisabled = (): boolean => { public isSaveButtonDisabled = (): boolean => {
if (this.state.hasErrors) { if (this.state.hasErrors || this.state.isSaving) {
return true; return true;
} }
for (const key of this.state.currentValues.keys()) { for (const key of this.state.currentValues.keys()) {
@ -264,38 +341,69 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return true; return true;
}; };
private performRefresh = async (): Promise<RefreshResult> => { private performRefresh = async (): Promise<void> => {
const refreshResult = await this.props.descriptor.onRefresh(); const refreshResult = await this.props.descriptor.onRefresh();
this.setState({ refreshResult: { ...refreshResult } }); let updateInProgressNotification: SelfServeNotification;
return refreshResult; if (this.state.refreshResult?.isUpdateInProgress && !refreshResult.isUpdateInProgress) {
await this.initializeSmartUiComponent();
}
if (refreshResult.isUpdateInProgress) {
updateInProgressNotification = {
type: MessageBarType.info,
isCancellable: false,
message: this.getTranslation(refreshResult.updateInProgressMessageTKey),
};
}
this.setState({
refreshResult: { ...refreshResult },
notification: updateInProgressNotification,
});
}; };
public onRefreshClicked = async (): Promise<void> => { public onRefreshClicked = async (): Promise<void> => {
this.setState({ isInitializing: true }); this.setState({ isInitializing: true });
const refreshResult = await this.performRefresh(); await this.performRefresh();
if (!refreshResult.isUpdateInProgress) {
this.initializeSmartUiComponent();
}
this.setState({ isInitializing: false }); this.setState({ isInitializing: false });
}; };
public getCommonTranslation = (translationFunction: TFunction, key: string): string => { public pollRefresh = async (): Promise<void> => {
return translationFunction(`Common.${key}`); try {
await this.performRefresh();
} catch (error) {
throw new AbortError(error);
}
const refreshResult = this.state.refreshResult;
if (refreshResult.isUpdateInProgress) {
throw new Error("update in progress. retrying ...");
}
}; };
private getCommandBarItems = (translate: TFunction): ICommandBarItemProps[] => { public getCommonTranslation = (key: string): string => {
return this.getTranslation(key, "Common");
};
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
const translationKey = `${prefix}.${messageKey}`;
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
if (translation === translationKey) {
return messageKey;
}
return translation;
};
private getCommandBarItems = (): ICommandBarItemProps[] => {
return [ return [
{ {
key: "save", key: "save",
text: this.getCommonTranslation(translate, "Save"), text: this.getCommonTranslation("Save"),
iconProps: { iconName: "Save" }, iconProps: { iconName: "Save" },
split: true, split: true,
disabled: this.isSaveButtonDisabled(), disabled: this.isSaveButtonDisabled(),
onClick: this.onSaveButtonClick, onClick: () => this.onSaveButtonClick(),
}, },
{ {
key: "discard", key: "discard",
text: this.getCommonTranslation(translate, "Discard"), text: this.getCommonTranslation("Discard"),
iconProps: { iconName: "Undo" }, iconProps: { iconName: "Undo" },
split: true, split: true,
disabled: this.isDiscardButtonDisabled(), disabled: this.isDiscardButtonDisabled(),
@ -305,7 +413,7 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
}, },
{ {
key: "refresh", key: "refresh",
text: this.getCommonTranslation(translate, "Refresh"), text: this.getCommonTranslation("Refresh"),
disabled: this.state.isInitializing, disabled: this.state.isInitializing,
iconProps: { iconName: "Refresh" }, iconProps: { iconName: "Refresh" },
split: true, split: true,
@ -316,12 +424,11 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
]; ];
}; };
private getNotificationMessageTranslation = (translationFunction: TFunction, messageKey: string): string => { private sendNotificationMessage = (portalNotificationContent: PortalNotificationContent): void => {
const translation = translationFunction(messageKey); sendMessage({
if (translation === `${this.smartUiGeneratorClassName}.${messageKey}`) { type: SelfServeMessageTypes.Notification,
return messageKey; data: { portalNotificationContent },
} });
return translation;
}; };
public render(): JSX.Element { public render(): JSX.Element {
@ -332,14 +439,14 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return ( return (
<Translation> <Translation>
{(translate) => { {(translate) => {
const getTranslation = (key: string): string => { if (!this.translationFunction) {
return translate(`${this.smartUiGeneratorClassName}.${key}`); this.translationFunction = translate;
}; }
return ( return (
<div style={{ overflowX: "auto" }}> <div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}> <Stack tokens={containerStackTokens} styles={{ root: { padding: 10 } }}>
<CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems(translate)} /> <CommandBar styles={{ root: { paddingLeft: 0 } }} items={this.getCommandBarItems()} />
{this.state.isInitializing ? ( {this.state.isInitializing ? (
<Spinner <Spinner
size={SpinnerSize.large} size={SpinnerSize.large}
@ -347,27 +454,25 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
/> />
) : ( ) : (
<> <>
{this.state.refreshResult?.isUpdateInProgress && (
<MessageBar messageBarType={MessageBarType.info} styles={{ root: { width: 400 } }}>
{getTranslation(this.state.refreshResult.notificationMessage)}
</MessageBar>
)}
{this.state.notification && ( {this.state.notification && (
<MessageBar <MessageBar
messageBarType={getMessageBarType(this.state.notification.type)} messageBarType={this.state.notification.type}
styles={{ root: { width: 400 } }} onDismiss={
onDismiss={() => this.setState({ notification: undefined })} this.state.notification.isCancellable
? () => this.setState({ notification: undefined })
: undefined
}
> >
{this.getNotificationMessageTranslation(getTranslation, this.state.notification.message)} {this.state.notification.message}
</MessageBar> </MessageBar>
)} )}
<SmartUiComponent <SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress} disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
descriptor={this.state.root as SmartUiDescriptor} descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues} currentValues={this.state.currentValues}
onInputChange={this.onInputChange} onInputChange={this.onInputChange}
onError={this.onError} onError={this.onError}
getTranslation={getTranslation} getTranslation={this.getTranslation}
/> />
</> </>
)} )}

View File

@ -1,56 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import Explorer from "../Explorer/Explorer";
import { SelfServeComponent } from "./SelfServeComponent";
import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
export class SelfServeComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<SelfServeDescriptor>;
public container: Explorer;
constructor(container: Explorer) {
this.container = container;
this.parameters = ko.observable(undefined);
this.container.selfServeType.subscribe(() => {
this.triggerRender();
});
}
public static getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
return new SqlX.default().toSelfServeDescriptor();
}
default:
return undefined;
}
};
public renderComponent(): JSX.Element {
if (this.container.selfServeType() === SelfServeType.invalid) {
return <h1>Invalid self serve type!</h1>;
}
const smartUiDescriptor = this.parameters();
return smartUiDescriptor ? <SelfServeComponent descriptor={smartUiDescriptor} /> : <></>;
}
private triggerRender() {
window.requestAnimationFrame(async () => {
const selfServeType = this.container.selfServeType();
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
this.parameters(smartUiDescriptor);
});
}
}

View File

@ -1,25 +0,0 @@
/**
* This adapter is responsible to render the React component
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
* and update any knockout observables passed from the parent.
*/
import * as ko from "knockout";
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
import * as React from "react";
import { ReactAdapter } from "../Bindings/ReactBindingHandler";
export class SelfServeLoadingComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor() {
this.parameters = ko.observable(Date.now());
}
public renderComponent(): JSX.Element {
return <Spinner size={SpinnerSize.large} />;
}
private triggerRender() {
window.requestAnimationFrame(() => this.renderComponent());
}
}

View File

@ -3,7 +3,11 @@ interface BaseInput {
errorMessage?: string; errorMessage?: string;
type: InputTypeValue; type: InputTypeValue;
labelTKey?: (() => Promise<string>) | string; labelTKey?: (() => Promise<string>) | string;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; onChange?: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>;
placeholderTKey?: (() => Promise<string>) | string; placeholderTKey?: (() => Promise<string>) | string;
} }
@ -44,16 +48,23 @@ export interface Node {
export interface SelfServeDescriptor { export interface SelfServeDescriptor {
root: Node; root: Node;
initialize?: () => Promise<Map<string, SmartUiInput>>; initialize?: () => Promise<Map<string, SmartUiInput>>;
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; onSave?: (
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Promise<OnSaveResult>;
inputNames?: string[]; inputNames?: string[];
onRefresh?: () => Promise<RefreshResult>; onRefresh?: () => Promise<RefreshResult>;
refreshParams?: RefreshParams;
} }
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay; export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
export abstract class SelfServeBaseClass { export abstract class SelfServeBaseClass {
public abstract initialize: () => Promise<Map<string, SmartUiInput>>; public abstract initialize: () => Promise<Map<string, SmartUiInput>>;
public abstract onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; public abstract onSave: (
currentValues: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Promise<OnSaveResult>;
public abstract onRefresh: () => Promise<RefreshResult>; public abstract onRefresh: () => Promise<RefreshResult>;
public toSelfServeDescriptor(): SelfServeDescriptor { public toSelfServeDescriptor(): SelfServeDescriptor {
@ -70,7 +81,7 @@ export abstract class SelfServeBaseClass {
throw new Error(`onRefresh() was not declared for the class '${className}'`); throw new Error(`onRefresh() was not declared for the class '${className}'`);
} }
if (!selfServeDescriptor?.root) { if (!selfServeDescriptor?.root) {
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`); throw new Error(`@IsDisplayable decorator was not declared for the class '${className}'`);
} }
selfServeDescriptor.initialize = this.initialize; selfServeDescriptor.initialize = this.initialize;
@ -89,7 +100,7 @@ export enum NumberUiType {
export type ChoiceItem = { label: string; key: string }; export type ChoiceItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem; export type InputType = number | string | boolean | ChoiceItem | Description;
export interface Info { export interface Info {
messageTKey: string; messageTKey: string;
@ -99,8 +110,15 @@ export interface Info {
}; };
} }
export enum DescriptionType {
Text,
InfoMessageBar,
WarningMessageBar,
}
export interface Description { export interface Description {
textTKey: string; textTKey: string;
type: DescriptionType;
link?: { link?: {
href: string; href: string;
textTKey: string; textTKey: string;
@ -113,18 +131,29 @@ export interface SmartUiInput {
disabled?: boolean; disabled?: boolean;
} }
export enum SelfServeNotificationType { export interface OnSaveResult {
info = "info", operationStatusUrl: string;
warning = "warning", portalNotification?: {
error = "error", initialize: {
} titleTKey: string;
messageTKey: string;
export interface SelfServeNotification { };
message: string; success: {
type: SelfServeNotificationType; titleTKey: string;
messageTKey: string;
};
failure: {
titleTKey: string;
messageTKey: string;
};
};
} }
export interface RefreshResult { export interface RefreshResult {
isUpdateInProgress: boolean; isUpdateInProgress: boolean;
notificationMessage: string; updateInProgressMessageTKey: string;
}
export interface RefreshParams {
retryIntervalInMs: number;
} }

View File

@ -1,11 +1,11 @@
import { NumberUiType, RefreshResult, SelfServeBaseClass, SelfServeNotification, SmartUiInput } from "./SelfServeTypes"; import { NumberUiType, OnSaveResult, RefreshResult, SelfServeBaseClass, SmartUiInput } from "./SelfServeTypes";
import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils"; import { DecoratorProperties, mapToSmartUiDescriptor, updateContextWithDecorator } from "./SelfServeUtils";
describe("SelfServeUtils", () => { describe("SelfServeUtils", () => {
it("initialize should be declared for self serve classes", () => { it("initialize should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize: () => Promise<Map<string, SmartUiInput>>; public initialize: () => Promise<Map<string, SmartUiInput>>;
public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<SelfServeNotification>; public onSave: (currentValues: Map<string, SmartUiInput>) => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>; public onRefresh: () => Promise<RefreshResult>;
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
@ -14,7 +14,7 @@ describe("SelfServeUtils", () => {
it("onSave should be declared for self serve classes", () => { it("onSave should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize = jest.fn(); public initialize = jest.fn();
public onSave: () => Promise<SelfServeNotification>; public onSave: () => Promise<OnSaveResult>;
public onRefresh: () => Promise<RefreshResult>; public onRefresh: () => Promise<RefreshResult>;
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("onSave() was not declared for the class 'Test'");
@ -29,14 +29,14 @@ describe("SelfServeUtils", () => {
expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'"); expect(() => new Test().toSelfServeDescriptor()).toThrow("onRefresh() was not declared for the class 'Test'");
}); });
it("@SmartUi decorator must be present for self serve classes", () => { it("@IsDisplayable decorator must be present for self serve classes", () => {
class Test extends SelfServeBaseClass { class Test extends SelfServeBaseClass {
public initialize = jest.fn(); public initialize = jest.fn();
public onSave = jest.fn(); public onSave = jest.fn();
public onRefresh = jest.fn(); public onRefresh = jest.fn();
} }
expect(() => new Test().toSelfServeDescriptor()).toThrow( expect(() => new Test().toSelfServeDescriptor()).toThrow(
"@SmartUi decorator was not declared for the class 'Test'" "@IsDisplayable decorator was not declared for the class 'Test'"
); );
}); });

View File

@ -1,4 +1,3 @@
import { MessageBarType } from "office-ui-fabric-react";
import "reflect-metadata"; import "reflect-metadata";
import { import {
Node, Node,
@ -15,8 +14,9 @@ import {
SelfServeDescriptor, SelfServeDescriptor,
SmartUiInput, SmartUiInput,
StringInput, StringInput,
SelfServeNotificationType, RefreshParams,
} from "./SelfServeTypes"; } from "./SelfServeTypes";
import { userContext } from "../UserContext";
export enum SelfServeType { export enum SelfServeType {
// No self serve type passed, launch explorer // No self serve type passed, launch explorer
@ -28,6 +28,14 @@ export enum SelfServeType {
sqlx = "sqlx", sqlx = "sqlx",
} }
export enum BladeType {
SqlKeys = "keys",
MongoKeys = "mongoDbKeys",
CassandraKeys = "cassandraDbKeys",
GremlinKeys = "keys",
TableKeys = "tableKeys",
}
export interface DecoratorProperties { export interface DecoratorProperties {
id: string; id: string;
info?: (() => Promise<Info>) | Info; info?: (() => Promise<Info>) | Info;
@ -44,9 +52,13 @@ export interface DecoratorProperties {
uiType?: string; uiType?: string;
errorMessage?: string; errorMessage?: string;
description?: (() => Promise<Description>) | Description; description?: (() => Promise<Description>) | Description;
onChange?: (currentState: Map<string, SmartUiInput>, newValue: InputType) => Map<string, SmartUiInput>; isDynamicDescription?: boolean;
onSave?: (currentValues: Map<string, SmartUiInput>) => Promise<void>; refreshParams?: RefreshParams;
initialize?: () => Promise<Map<string, SmartUiInput>>; onChange?: (
newValue: InputType,
currentState: Map<string, SmartUiInput>,
baselineValues: ReadonlyMap<string, SmartUiInput>
) => Map<string, SmartUiInput>;
} }
const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>( const setValue = <T extends keyof DecoratorProperties, K extends DecoratorProperties[T]>(
@ -83,7 +95,7 @@ export const updateContextWithDecorator = <T extends keyof DecoratorProperties,
descriptorValue: K descriptorValue: K
): void => { ): void => {
if (!(context instanceof Map)) { if (!(context instanceof Map)) {
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`); throw new Error(`@IsDisplayable should be the first decorator for the class '${className}'.`);
} }
const propertyObject = context.get(propertyName) ?? { id: propertyName }; const propertyObject = context.get(propertyName) ?? { id: propertyName };
@ -108,16 +120,17 @@ export const mapToSmartUiDescriptor = (
className: string, className: string,
context: Map<string, DecoratorProperties> context: Map<string, DecoratorProperties>
): SelfServeDescriptor => { ): SelfServeDescriptor => {
const inputNames: string[] = [];
const root = context.get("root"); const root = context.get("root");
context.delete("root"); context.delete("root");
const inputNames: string[] = [];
const smartUiDescriptor: SelfServeDescriptor = { const smartUiDescriptor: SelfServeDescriptor = {
root: { root: {
id: className, id: className,
info: root?.info, info: undefined,
children: [], children: [],
}, },
refreshParams: root?.refreshParams,
}; };
while (context.size > 0) { while (context.size > 0) {
@ -155,7 +168,10 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
} }
return value as NumberInput; return value as NumberInput;
case "string": case "string":
if (value.description) { if (value.description || value.isDynamicDescription) {
if (value.description && value.isDynamicDescription) {
value.errorMessage = `dynamic descriptions should not have defaults set here.`;
}
return value as DescriptionDisplay; return value as DescriptionDisplay;
} }
if (!value.labelTKey) { if (!value.labelTKey) {
@ -175,13 +191,9 @@ const getInput = (value: DecoratorProperties): AnyDisplay => {
} }
}; };
export const getMessageBarType = (type: SelfServeNotificationType): MessageBarType => { export const generateBladeLink = (blade: BladeType): string => {
switch (type) { const subscriptionId = userContext.subscriptionId;
case SelfServeNotificationType.info: const resourceGroupName = userContext.resourceGroup;
return MessageBarType.info; const databaseAccountName = userContext.databaseAccount.name;
case SelfServeNotificationType.warning: return `www.portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
return MessageBarType.warning;
case SelfServeNotificationType.error:
return MessageBarType.error;
}
}; };

View File

@ -1,18 +1,19 @@
import { IsDisplayable, OnChange, Values } from "../Decorators"; import { IsDisplayable, OnChange, Values } from "../Decorators";
import { import {
ChoiceItem, ChoiceItem,
DescriptionType,
InputType, InputType,
NumberUiType, NumberUiType,
OnSaveResult,
RefreshResult, RefreshResult,
SelfServeBaseClass, SelfServeBaseClass,
SelfServeNotification,
SmartUiInput, SmartUiInput,
} from "../SelfServeTypes"; } from "../SelfServeTypes";
import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp"; import { refreshDedicatedGatewayProvisioning } from "./SqlX.rp";
const onEnableDedicatedGatewayChange = ( const onEnableDedicatedGatewayChange = (
currentState: Map<string, SmartUiInput>, newValue: InputType,
newValue: InputType currentState: Map<string, SmartUiInput>
): Map<string, SmartUiInput> => { ): Map<string, SmartUiInput> => {
const sku = currentState.get("sku"); const sku = currentState.get("sku");
const instances = currentState.get("instances"); const instances = currentState.get("instances");
@ -49,7 +50,7 @@ export default class SqlX extends SelfServeBaseClass {
return refreshDedicatedGatewayProvisioning(); return refreshDedicatedGatewayProvisioning();
}; };
public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<SelfServeNotification> => { public onSave = async (currentValues: Map<string, SmartUiInput>): Promise<OnSaveResult> => {
validate(currentValues); validate(currentValues);
// TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call. // TODO: add pre processing logic before calling the updateDedicatedGatewayProvisioning() RP call.
throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`); throw new Error(`onSave not implemented. No. of properties to save: ${currentValues.size}`);
@ -63,6 +64,7 @@ export default class SqlX extends SelfServeBaseClass {
@Values({ @Values({
description: { description: {
textTKey: "Provisioning dedicated gateways for SqlX accounts.", textTKey: "Provisioning dedicated gateways for SqlX accounts.",
type: DescriptionType.Text,
link: { link: {
href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction", href: "https://docs.microsoft.com/en-us/azure/cosmos-db/introduction",
textTKey: "Learn more about dedicated gateway.", textTKey: "Learn more about dedicated gateway.",

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="selfServeViewport" content="height=device-height, width=device-width, initial-scale=1.0" />
<title>Self Serve</title>
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
</head>
<body>
<div class="selfServeComponentContainer" id="selfServeContent"></div>
</body>
</html>

View File

@ -1,4 +1,4 @@
import { StringUtility } from "./StringUtility"; import * as StringUtility from "./StringUtility";
export class LocalStorageUtility { export class LocalStorageUtility {
public static hasItem(key: StorageKey): boolean { public static hasItem(key: StorageKey): boolean {

View File

@ -1,4 +1,4 @@
import { StringUtility } from "./StringUtility"; import * as StringUtility from "./StringUtility";
describe("String utility", () => { describe("String utility", () => {
it("Convert to integer from string", () => { it("Convert to integer from string", () => {

View File

@ -1,9 +1,7 @@
export class StringUtility { export function toNumber(num: string | null): number {
public static toNumber(num: string | null): number {
return Number(num); return Number(num);
} }
public static toBoolean(valueStr: string | null): boolean { export function toBoolean(valueStr: string | null): boolean {
return valueStr === "true"; return valueStr === "true";
} }
}

View File

@ -17,12 +17,43 @@ interface UserContext {
useSDKOperations?: boolean; useSDKOperations?: boolean;
subscriptionType?: SubscriptionType; subscriptionType?: SubscriptionType;
quotaId?: string; quotaId?: string;
// API Type is not yet provided by ARM. You need to manually inspect all the capabilities+kind so we abstract that logic in userContext
// This is coming in a future Cosmos ARM API version as a prperty on databaseAccount
apiType?: ApiType;
} }
const userContext: Readonly<UserContext> = {} as const; type ApiType = "SQL" | "Mongo" | "Gremlin" | "Tables" | "Cassandra";
const userContext: UserContext = {};
function updateUserContext(newContext: UserContext): void { function updateUserContext(newContext: UserContext): void {
Object.assign(userContext, newContext); Object.assign(userContext, newContext);
Object.assign(userContext, { apiType: apiType(userContext.databaseAccount) });
}
function apiType(account: DatabaseAccount | undefined): ApiType {
if (!account) {
return "SQL";
}
const capabilities = account.properties?.capabilities;
if (capabilities) {
if (capabilities.find((c) => c.name === "EnableCassandra")) {
return "Cassandra";
}
if (capabilities.find((c) => c.name === "EnableGremlin")) {
return "Gremlin";
}
if (capabilities.find((c) => c.name === "EnableMongo")) {
return "Mongo";
}
if (capabilities.find((c) => c.name === "EnableTable")) {
return "Tables";
}
}
if (account.kind === "MongoDB" || account.kind === "Parse") {
return "Mongo";
}
return "SQL";
} }
export { userContext, updateUserContext }; export { userContext, updateUserContext };

View File

@ -1,9 +1,7 @@
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as AuthorizationUtils from "./AuthorizationUtils"; import * as AuthorizationUtils from "./AuthorizationUtils";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import Explorer from "../Explorer/Explorer";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { Platform, updateConfigContext } from "../ConfigContext";
jest.mock("../Explorer/Explorer"); jest.mock("../Explorer/Explorer");
describe("AuthorizationUtils", () => { describe("AuthorizationUtils", () => {
@ -34,10 +32,6 @@ describe("AuthorizationUtils", () => {
expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError(); expect(() => AuthorizationUtils.decryptJWTToken(undefined)).toThrowError();
}); });
it("should throw an error if token is null", () => {
expect(() => AuthorizationUtils.decryptJWTToken(null)).toThrowError();
});
it("should throw an error if token is empty", () => { it("should throw an error if token is empty", () => {
expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError(); expect(() => AuthorizationUtils.decryptJWTToken("")).toThrowError();
}); });

View File

@ -1,7 +1,6 @@
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants"; import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger"; import * as Logger from "../Common/Logger";
import { configContext, Platform } from "../ConfigContext";
import * as ViewModels from "../Contracts/ViewModels"; import * as ViewModels from "../Contracts/ViewModels";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";

View File

@ -13,6 +13,8 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor"; import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { Notebook } from "@nteract/commutable";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
const defaultSelectedAbuseCategory = "Other"; const defaultSelectedAbuseCategory = "Other";
const abuseCategories: IChoiceGroupOption[] = [ const abuseCategories: IChoiceGroupOption[] = [
@ -243,7 +245,10 @@ export function downloadItem(
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`); throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
} }
await container.importAndOpenContent(data.name, response.data); const notebook = JSON.parse(response.data) as Notebook;
removeNotebookViewerLink(notebook, data.newCellId);
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
NotificationConsoleUtils.logConsoleMessage( NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Info, ConsoleDataType.Info,
`Successfully downloaded ${name} to My Notebooks` `Successfully downloaded ${name} to My Notebooks`
@ -281,6 +286,17 @@ export function downloadItem(
); );
} }
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4?.cells[0]?.source[0]?.search(newCellId)) {
notebookV4.cells.splice(0, 1);
notebook = notebookV4;
}
};
export async function favoriteItem( export async function favoriteItem(
container: Explorer, container: Explorer,
junoClient: JunoClient, junoClient: JunoClient,
@ -373,7 +389,9 @@ export function deleteItem(
container: Explorer, container: Explorer,
junoClient: JunoClient, junoClient: JunoClient,
data: IGalleryItem, data: IGalleryItem,
onComplete: (item: IGalleryItem) => void onComplete: (item: IGalleryItem) => void,
beforeDelete?: () => void,
afterDelete?: () => void
): void { ): void {
if (container) { if (container) {
trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id }); trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id });
@ -383,6 +401,9 @@ export function deleteItem(
`Would you like to remove ${data.name} from the gallery?`, `Would you like to remove ${data.name} from the gallery?`,
"Remove", "Remove",
async () => { async () => {
if (beforeDelete) {
beforeDelete();
}
const name = data.name; const name = data.name;
const notificationId = NotificationConsoleUtils.logConsoleMessage( const notificationId = NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.InProgress, ConsoleDataType.InProgress,
@ -409,6 +430,10 @@ export function deleteItem(
); );
handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`); handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`);
} finally {
if (afterDelete) {
afterDelete();
}
} }
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId); NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
@ -449,10 +474,10 @@ export function getNotebookViewerProps(search: string): NotebookViewerProps {
export function getTabTitle(tab: GalleryTab): string { export function getTabTitle(tab: GalleryTab): string {
switch (tab) { switch (tab) {
case GalleryTab.OfficialSamples:
return GalleryViewerComponent.OfficialSamplesTitle;
case GalleryTab.PublicGallery: case GalleryTab.PublicGallery:
return GalleryViewerComponent.PublicGalleryTitle; return GalleryViewerComponent.PublicGalleryTitle;
case GalleryTab.OfficialSamples:
return GalleryViewerComponent.OfficialSamplesTitle;
case GalleryTab.Favorites: case GalleryTab.Favorites:
return GalleryViewerComponent.FavoritesTitle; return GalleryViewerComponent.FavoritesTitle;
case GalleryTab.Published: case GalleryTab.Published:

View File

@ -1,6 +1,6 @@
import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent"; import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent";
import { IPinnedRepo } from "../Juno/JunoClient"; import { IPinnedRepo } from "../Juno/JunoClient";
import { JunoUtils } from "./JunoUtils"; import * as JunoUtils from "./JunoUtils";
import { IGitHubRepo } from "../GitHub/GitHubClient"; import { IGitHubRepo } from "../GitHub/GitHubClient";
const gitHubRepo: IGitHubRepo = { const gitHubRepo: IGitHubRepo = {

View File

@ -2,8 +2,7 @@ import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent";
import { IGitHubRepo } from "../GitHub/GitHubClient"; import { IGitHubRepo } from "../GitHub/GitHubClient";
import { IPinnedRepo } from "../Juno/JunoClient"; import { IPinnedRepo } from "../Juno/JunoClient";
export class JunoUtils { export function toPinnedRepo(item: RepoListItem): IPinnedRepo {
public static toPinnedRepo(item: RepoListItem): IPinnedRepo {
return { return {
owner: item.repo.owner, owner: item.repo.owner,
name: item.repo.name, name: item.repo.name,
@ -12,11 +11,10 @@ export class JunoUtils {
}; };
} }
public static toGitHubRepo(pinnedRepo: IPinnedRepo): IGitHubRepo { export function toGitHubRepo(pinnedRepo: IPinnedRepo): IGitHubRepo {
return { return {
owner: pinnedRepo.owner, owner: pinnedRepo.owner,
name: pinnedRepo.name, name: pinnedRepo.name,
private: pinnedRepo.private, private: pinnedRepo.private,
}; };
} }
}

View File

@ -8,15 +8,46 @@ interface KernelConnectionMetadata {
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo; notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo;
} }
export class NotebookConfigurationUtils { export const _configureServiceEndpoints = async (kernelMetadata: KernelConnectionMetadata): Promise<void> => {
private constructor() {} if (!kernelMetadata) {
// should never get into this state
Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
return;
}
public static async configureServiceEndpoints( const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
const configurationEndpoints = kernelMetadata.configurationEndpoints;
if (notebookConnectionInfo && configurationEndpoints) {
try {
const headers: HeadersInit = { "Content-Type": "application/json" };
if (notebookConnectionInfo.authToken) {
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
}
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
method: "POST",
headers,
body: JSON.stringify(configurationEndpoints),
});
if (!response.ok) {
const responseMessage = await response.json();
Logger.logError(
getErrorMessage(responseMessage),
"NotebookConfigurationUtils/configureServiceEndpoints",
response.status
);
}
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
}
}
};
export const configureServiceEndpoints = async (
notebookPath: string, notebookPath: string,
notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo, notebookConnectionInfo: DataModels.NotebookWorkspaceConnectionInfo,
kernelName: string, kernelName: string,
clusterConnectionInfo: DataModels.SparkClusterConnectionInfo clusterConnectionInfo: DataModels.SparkClusterConnectionInfo
): Promise<void> { ): Promise<void> => {
if (!notebookPath || !notebookConnectionInfo || !kernelName) { if (!notebookPath || !notebookConnectionInfo || !kernelName) {
Logger.logError( Logger.logError(
"Invalid or missing notebook connection info/path", "Invalid or missing notebook connection info/path",
@ -53,40 +84,5 @@ export class NotebookConfigurationUtils {
name: kernelName, name: kernelName,
}; };
return await NotebookConfigurationUtils._configureServiceEndpoints(kernelMetadata); return await _configureServiceEndpoints(kernelMetadata);
} };
private static async _configureServiceEndpoints(kernelMetadata: KernelConnectionMetadata): Promise<void> {
if (!kernelMetadata) {
// should never get into this state
Logger.logWarning("kernel metadata is null or undefined", "NotebookConfigurationUtils/configureServiceEndpoints");
return;
}
const notebookConnectionInfo = kernelMetadata.notebookConnectionInfo;
const configurationEndpoints = kernelMetadata.configurationEndpoints;
if (notebookConnectionInfo && configurationEndpoints) {
try {
const headers: any = { "Content-Type": "application/json" };
if (notebookConnectionInfo.authToken) {
headers["Authorization"] = `token ${notebookConnectionInfo.authToken}`;
}
const response = await fetch(`${notebookConnectionInfo.notebookServerEndpoint}/api/configureEndpoints`, {
method: "POST",
headers,
body: JSON.stringify(configurationEndpoints),
});
if (!response.ok) {
const responseMessage = await response.json();
Logger.logError(
getErrorMessage(responseMessage),
"NotebookConfigurationUtils/configureServiceEndpoints",
response.status
);
}
} catch (error) {
Logger.logError(getErrorMessage(error), "NotebookConfigurationUtils/configureServiceEndpoints");
}
}
}
}

View File

@ -1,4 +1,4 @@
import { StringUtils } from "./StringUtils"; import * as StringUtils from "./StringUtils";
describe("StringUtils", () => { describe("StringUtils", () => {
describe("stripSpacesFromString()", () => { describe("stripSpacesFromString()", () => {
@ -12,9 +12,9 @@ describe("StringUtils", () => {
expect(transformedString).toBe("abc"); expect(transformedString).toBe("abc");
}); });
it("should return null if input is null", () => { it("should return undefined if input is undefined", () => {
const transformedString: string = StringUtils.stripSpacesFromString(null); const transformedString: string = StringUtils.stripSpacesFromString(undefined);
expect(transformedString).toBeNull(); expect(transformedString).toBeUndefined();
}); });
it("should return undefined if input is undefiend", () => { it("should return undefined if input is undefiend", () => {

View File

@ -1,6 +1,5 @@
export class StringUtils { export function stripSpacesFromString(inputString: string): string {
public static stripSpacesFromString(inputString: string): string { if (inputString === undefined || typeof inputString !== "string") {
if (inputString == null || typeof inputString !== "string") {
return inputString; return inputString;
} }
return inputString.replace(/ /g, ""); return inputString.replace(/ /g, "");
@ -11,11 +10,10 @@ export class StringUtils {
* @param stringToTest * @param stringToTest
* @param suffix * @param suffix
*/ */
public static endsWith(stringToTest: string, suffix: string): boolean { export function endsWith(stringToTest: string, suffix: string): boolean {
return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1; return stringToTest.indexOf(suffix, stringToTest.length - suffix.length) !== -1;
} }
public static startsWith(stringToTest: string, prefix: string): boolean { export function startsWith(stringToTest: string, prefix: string): boolean {
return stringToTest.indexOf(prefix) === 0; return stringToTest.indexOf(prefix) === 0;
} }
}

View File

@ -47,15 +47,14 @@ interface Options {
queryParams?: ARMQueryParams; queryParams?: ARMQueryParams;
} }
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them. export async function armRequestWithoutPolling<T>({
export async function armRequest<T>({
host, host,
path, path,
apiVersion, apiVersion,
method, method,
body: requestBody, body: requestBody,
queryParams, queryParams,
}: Options): Promise<T> { }: Options): Promise<{ result: T; operationStatusUrl: string }> {
const url = new URL(path, host); const url = new URL(path, host);
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion); url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
if (queryParams) { if (queryParams) {
@ -92,13 +91,33 @@ export async function armRequest<T>({
throw error; throw error;
} }
const operationStatusUrl = response.headers && response.headers.get("location"); const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
const responseBody = (await response.json()) as T;
return { result: responseBody, operationStatusUrl: operationStatusUrl };
}
// TODO: This is very similar to what is happening in ResourceProviderClient.ts. Should probably merge them.
export async function armRequest<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
}: Options): Promise<T> {
const armRequestResult = await armRequestWithoutPolling<T>({
host,
path,
apiVersion,
method,
body: requestBody,
queryParams,
});
const operationStatusUrl = armRequestResult.operationStatusUrl;
if (operationStatusUrl) { if (operationStatusUrl) {
return await promiseRetry(() => getOperationStatus(operationStatusUrl)); return await promiseRetry(() => getOperationStatus(operationStatusUrl));
} }
return armRequestResult.result;
const responseBody = (await response.json()) as T;
return responseBody;
} }
async function getOperationStatus(operationStatusUrl: string) { async function getOperationStatus(operationStatusUrl: string) {

13
src/global.d.ts vendored
View File

@ -1,11 +1,22 @@
import { AuthType } from "./AuthType";
import Explorer from "./Explorer/Explorer"; import Explorer from "./Explorer/Explorer";
declare global { declare global {
interface Window { interface Window {
/**
* @deprecated
* DO NOT take new usage of window.dataExplorer. If you must use Explorer, find it directly.
* */
dataExplorer: Explorer; dataExplorer: Explorer;
__REACT_DEVTOOLS_GLOBAL_HOOK__: any; __REACT_DEVTOOLS_GLOBAL_HOOK__: any;
/**
* @deprecated
* No new usage of jQuery ($)
* */
$: any; $: any;
/**
* @deprecated
* No new usage of jQuery
* */
jQuery: any; jQuery: any;
gitSha: string; gitSha: string;
} }

View File

@ -2,8 +2,9 @@ import { useEffect } from "react";
import { applyExplorerBindings } from "../applyExplorerBindings"; import { applyExplorerBindings } from "../applyExplorerBindings";
import { AuthType } from "../AuthType"; import { AuthType } from "../AuthType";
import { AccountKind, DefaultAccountExperience } from "../Common/Constants"; import { AccountKind, DefaultAccountExperience } from "../Common/Constants";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendMessage } from "../Common/MessageHandler"; import { sendMessage } from "../Common/MessageHandler";
import { configContext, Platform } from "../ConfigContext"; 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";
@ -23,7 +24,6 @@ import {
getDatabaseAccountKindFromExperience, getDatabaseAccountKindFromExperience,
getDatabaseAccountPropertiesFromMetadata, getDatabaseAccountPropertiesFromMetadata,
} from "../Platform/Hosted/HostedUtils"; } from "../Platform/Hosted/HostedUtils";
import { SelfServeType } from "../SelfServe/SelfServeUtils";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility"; import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { updateUserContext } from "../UserContext"; import { updateUserContext } from "../UserContext";
import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts"; import { listKeys } from "../Utils/arm/generatedClients/2020-04-01/databaseAccounts";
@ -57,7 +57,6 @@ export function useKnockoutExplorer(platform: Platform, explorerParams: Explorer
async function configureHosted() { async function configureHosted() {
const win = (window as unknown) as HostedExplorerChildFrame; const win = (window as unknown) as HostedExplorerChildFrame;
explorer.selfServeType(SelfServeType.none);
if (win.hostedConfig.authType === AuthType.EncryptedToken) { if (win.hostedConfig.authType === AuthType.EncryptedToken) {
configureHostedWithEncryptedToken(win.hostedConfig); configureHostedWithEncryptedToken(win.hostedConfig);
} else if (win.hostedConfig.authType === AuthType.ResourceToken) { } else if (win.hostedConfig.authType === AuthType.ResourceToken) {
@ -91,20 +90,24 @@ async function configureHostedWithAAD(config: AAD) {
} }
function configureHostedWithConnectionString(config: ConnectionString) { function configureHostedWithConnectionString(config: ConnectionString) {
updateUserContext({
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken),
});
const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind); const apiExperience = DefaultExperienceUtility.getDefaultExperienceFromApiKind(config.encryptedTokenMetadata.apiKind);
explorer.configure({ const databaseAccount = {
databaseAccount: {
id: "", id: "",
location: "",
type: "",
name: config.encryptedTokenMetadata.accountName, name: config.encryptedTokenMetadata.accountName,
kind: getDatabaseAccountKindFromExperience(apiExperience), kind: getDatabaseAccountKindFromExperience(apiExperience),
properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata), properties: getDatabaseAccountPropertiesFromMetadata(config.encryptedTokenMetadata),
tags: {}, tags: {},
}, };
updateUserContext({
// For legacy reasons lots of code expects a connection string login to look and act like an encrypted token login
authType: AuthType.EncryptedToken,
accessToken: encodeURIComponent(config.encryptedToken),
databaseAccount,
});
explorer.configure({
databaseAccount,
masterKey: config.masterKey, masterKey: config.masterKey,
features: extractFeatures(), features: extractFeatures(),
}); });
@ -112,7 +115,18 @@ function configureHostedWithConnectionString(config: ConnectionString) {
function configureHostedWithResourceToken(config: ResourceToken) { function configureHostedWithResourceToken(config: ResourceToken) {
const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken); const parsedResourceToken = parseResourceTokenConnectionString(config.resourceToken);
const databaseAccount = {
id: "",
location: "",
type: "",
name: parsedResourceToken.accountEndpoint,
kind: AccountKind.GlobalDocumentDB,
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
// Resource tokens can only be used with SQL API
tags: { defaultExperience: DefaultAccountExperience.DocumentDB },
};
updateUserContext({ updateUserContext({
databaseAccount,
authType: AuthType.ResourceToken, authType: AuthType.ResourceToken,
resourceToken: parsedResourceToken.resourceToken, resourceToken: parsedResourceToken.resourceToken,
endpoint: parsedResourceToken.accountEndpoint, endpoint: parsedResourceToken.accountEndpoint,
@ -123,14 +137,7 @@ function configureHostedWithResourceToken(config: ResourceToken) {
explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey); explorer.resourceTokenPartitionKey(parsedResourceToken.partitionKey);
} }
explorer.configure({ explorer.configure({
databaseAccount: { databaseAccount,
id: "",
name: parsedResourceToken.accountEndpoint,
kind: AccountKind.GlobalDocumentDB,
properties: { documentEndpoint: parsedResourceToken.accountEndpoint },
// Resource tokens can only be used with SQL API
tags: { defaultExperience: DefaultAccountExperience.DocumentDB },
},
features: extractFeatures(), features: extractFeatures(),
isAuthWithresourceToken: true, isAuthWithresourceToken: true,
}); });
@ -159,9 +166,9 @@ function configureHostedWithEncryptedToken(config: EncryptedToken) {
function configureEmulator() { function configureEmulator() {
updateUserContext({ updateUserContext({
databaseAccount: emulatorAccount,
authType: AuthType.MasterKey, authType: AuthType.MasterKey,
}); });
explorer.selfServeType(SelfServeType.none);
explorer.databaseAccount(emulatorAccount); explorer.databaseAccount(emulatorAccount);
explorer.isAccountReady(true); explorer.isAccountReady(true);
} }
@ -210,6 +217,25 @@ function configurePortal() {
inputs.extensionEndpoint = configContext.PROXY_PATH; inputs.extensionEndpoint = configContext.PROXY_PATH;
} }
const authorizationToken = inputs.authorizationToken || "";
const masterKey = inputs.masterKey || "";
const databaseAccount = inputs.databaseAccount;
updateConfigContext({
BACKEND_ENDPOINT: inputs.extensionEndpoint || configContext.BACKEND_ENDPOINT,
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
});
updateUserContext({
authorizationToken,
masterKey,
databaseAccount,
resourceGroup: inputs.resourceGroup,
subscriptionId: inputs.subscriptionId,
subscriptionType: inputs.subscriptionType,
quotaId: inputs.quotaId,
});
explorer.configure(inputs); explorer.configure(inputs);
applyExplorerBindings(explorer); applyExplorerBindings(explorer);
if (openAction) { if (openAction) {

View File

@ -1,6 +1,6 @@
import "expect-puppeteer"; import "expect-puppeteer";
import { Frame } from "puppeteer"; import { Frame } from "puppeteer";
import { generateUniqueName, login } from "../utils/shared"; import { generateDatabaseName, generateUniqueName, login } from "../utils/shared";
jest.setTimeout(300000); jest.setTimeout(300000);
const LOADING_STATE_DELAY = 2500; const LOADING_STATE_DELAY = 2500;
@ -11,7 +11,7 @@ const RENDER_DELAY = 1000;
describe("Collection Add and Delete Mongo spec", () => { describe("Collection Add and Delete Mongo spec", () => {
it("creates a collection", async () => { it("creates a collection", async () => {
try { try {
const dbId = generateUniqueName("db"); const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const sharedKey = `${generateUniqueName()}`; const sharedKey = `${generateUniqueName()}`;
const frame = await login(process.env.MONGO_CONNECTION_STRING); const frame = await login(process.env.MONGO_CONNECTION_STRING);

View File

@ -4,8 +4,7 @@ import { createDatabase, onClickSaveButton } from "../utils/shared";
import { generateUniqueName } from "../utils/shared"; import { generateUniqueName } from "../utils/shared";
import { ApiKind } from "../../src/Contracts/DataModels"; import { ApiKind } from "../../src/Contracts/DataModels";
const LOADING_STATE_DELAY = 3000; const LOADING_STATE_DELAY = 5000;
const CREATE_DELAY = 5000;
jest.setTimeout(300000); jest.setTimeout(300000);
describe("MongoDB Index policy tests", () => { describe("MongoDB Index policy tests", () => {
@ -21,29 +20,21 @@ describe("MongoDB Index policy tests", () => {
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true }); await frame.waitForSelector('div[class="splashScreen"] > div[class="title"]', { visible: true });
let databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`); const dbId = await createDatabase(frame);
if (databases.length === 0) { await frame.waitFor(25000);
await createDatabase(frame);
databases = await frame.$$(`div[class="databaseHeader main1 nodeItem "] > div[class="treeNodeHeader "]`);
}
const selectedDbId = await frame.evaluate((element) => {
return element.attributes["data-test"].textContent;
}, databases[0]);
// click on database // click on database
await frame.waitFor(`div[data-test="${selectedDbId}"]`); await frame.waitForSelector(`div[data-test="${dbId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${selectedDbId}"]`); await frame.click(`div[data-test="${dbId}"]`);
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
// click on scale & setting // click on scale & setting
const containers = await frame.$$( const containers = await frame.$$(
`div[class="nodeChildren"] > div[class="collectionHeader main2 nodeItem "]> div[class="treeNodeHeader "]` `div[class="nodeChildren"] > div[class="collectionHeader main2 nodeItem "]> div[class="treeNodeHeader "]`
); );
const selectedContainer = await frame.evaluate((element) => { const selectedContainer = (await frame.evaluate((element) => element.innerText, containers[0]))
return element.attributes["data-test"].textContent; .replace(/[\u{0080}-\u{FFFF}]/gu, "")
}, containers[0]); .trim();
await frame.waitFor(`div[data-test="${selectedContainer}"]`), { visible: true }; await frame.waitFor(`div[data-test="${selectedContainer}"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`div[data-test="${selectedContainer}"]`); await frame.click(`div[data-test="${selectedContainer}"]`);
@ -83,6 +74,7 @@ describe("MongoDB Index policy tests", () => {
let singleFieldIndexInserted = false, let singleFieldIndexInserted = false,
wildCardIndexInserted = false; wildCardIndexInserted = false;
await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true }; await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true };
await frame.waitFor(20000);
const elements = await frame.$$("div[data-automationid='DetailsRowCell'] > span"); const elements = await frame.$$("div[data-automationid='DetailsRowCell'] > span");
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
@ -94,7 +86,7 @@ describe("MongoDB Index policy tests", () => {
singleFieldIndexInserted = true; singleFieldIndexInserted = true;
} }
} }
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(20000);
expect(wildCardIndexInserted).toBe(true); expect(wildCardIndexInserted).toBe(true);
expect(singleFieldIndexInserted).toBe(true); expect(singleFieldIndexInserted).toBe(true);
@ -107,14 +99,14 @@ describe("MongoDB Index policy tests", () => {
await onClickSaveButton(frame); await onClickSaveButton(frame);
//check for cleaning //check for cleaning
await frame.waitFor(CREATE_DELAY); await frame.waitFor(20000);
await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true }; await frame.waitFor("div[data-automationid='DetailsRowCell'] > span"), { visible: true };
const isDeletionComplete = await frame.$$("div[data-automationid='DetailsRowCell'] > span"); const isDeletionComplete = await frame.$$("div[data-automationid='DetailsRowCell'] > span");
expect(isDeletionComplete).toHaveLength(2); expect(isDeletionComplete).toHaveLength(2);
} catch (error) { } catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName; const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` }); await page.screenshot({ path: `failed-${testName}.jpg` });
throw error; throw error;
} }
}); });

View File

@ -1,21 +0,0 @@
import { Frame } from "puppeteer";
import { ApiKind } from "../../src/Contracts/DataModels";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
jest.setTimeout(300000);
let frame: Frame;
describe("Mongo", () => {
it("Account opens", async () => {
try {
frame = await getTestExplorerFrame(ApiKind.MongoDB);
await frame.waitForSelector(".accordion");
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error;
}
});
});

View File

@ -20,6 +20,7 @@ describe("Self Serve", () => {
// id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE} // id of the display element is in the format {PROPERTY_NAME}-{DISPLAY_NAME}-{DISPLAY_TYPE}
await frame.waitForSelector("#description-text-display"); await frame.waitForSelector("#description-text-display");
await frame.waitForSelector("#currentRegionText-text-display");
const regions = await frame.waitForSelector("#regions-dropdown-input"); const regions = await frame.waitForSelector("#regions-dropdown-input");
let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]"); let disabledLoggingToggle = await frame.$$("#enableLogging-toggle-input[disabled]");

View File

@ -1,6 +1,6 @@
import "expect-puppeteer"; import "expect-puppeteer";
import { Frame } from "puppeteer"; import { Frame } from "puppeteer";
import { generateUniqueName, login } from "../utils/shared"; import { generateDatabaseName, generateUniqueName, login } from "../utils/shared";
jest.setTimeout(300000); jest.setTimeout(300000);
const LOADING_STATE_DELAY = 2500; const LOADING_STATE_DELAY = 2500;
@ -11,7 +11,7 @@ const RENDER_DELAY = 1000;
describe("Collection Add and Delete SQL spec", () => { describe("Collection Add and Delete SQL spec", () => {
it("creates a collection", async () => { it("creates a collection", async () => {
try { try {
const dbId = generateUniqueName("db"); const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const sharedKey = `/skey${generateUniqueName()}`; const sharedKey = `/skey${generateUniqueName()}`;
const frame = await login(process.env.PORTAL_RUNNER_CONNECTION_STRING); const frame = await login(process.env.PORTAL_RUNNER_CONNECTION_STRING);

View File

@ -1,8 +1,16 @@
/* eslint-disable jest/expect-expect */ /* eslint-disable jest/expect-expect */
import "expect-puppeteer"; import "expect-puppeteer";
import { Frame } from "puppeteer"; import { Frame } from "puppeteer";
import { generateUniqueName } from "../utils/shared"; import { generateDatabaseName, generateUniqueName } from "../utils/shared";
import { CosmosClient, PermissionMode } from "@azure/cosmos"; import { CosmosClient, PermissionMode } from "@azure/cosmos";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import * as msRestNodeAuth from "@azure/ms-rest-nodeauth";
const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners";
jest.setTimeout(300000); jest.setTimeout(300000);
const RETRY_DELAY = 5000; const RETRY_DELAY = 5000;
@ -10,11 +18,16 @@ const CREATE_DELAY = 10000;
describe("Collection Add and Delete SQL spec", () => { describe("Collection Add and Delete SQL spec", () => {
it("creates a collection", async () => { it("creates a collection", async () => {
const dbId = generateUniqueName("db"); const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const armClient = new CosmosDBManagementClient(credentials, subscriptionId);
const account = await armClient.databaseAccounts.get(resourceGroupName, "portal-sql-runner");
const keys = await armClient.databaseAccounts.listKeys(resourceGroupName, "portal-sql-runner");
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const connectionString = process.env.PORTAL_RUNNER_CONNECTION_STRING; const client = new CosmosClient({
const client = new CosmosClient(connectionString); endpoint: account.documentEndpoint,
const endpoint = /AccountEndpoint=(.*);/.exec(connectionString)[1]; key: keys.primaryMasterKey,
});
const { database } = await client.databases.createIfNotExists({ id: dbId }); const { database } = await client.databases.createIfNotExists({ id: dbId });
const { container } = await database.containers.createIfNotExists({ id: collectionId }); const { container } = await database.containers.createIfNotExists({ id: collectionId });
const { user } = await database.users.upsert({ id: "testUser" }); const { user } = await database.users.upsert({ id: "testUser" });
@ -23,7 +36,7 @@ describe("Collection Add and Delete SQL spec", () => {
permissionMode: PermissionMode.All, permissionMode: PermissionMode.All,
resource: container.url, resource: container.url,
}); });
const resourceTokenConnectionString = `AccountEndpoint=${endpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`; const resourceTokenConnectionString = `AccountEndpoint=${account.documentEndpoint};DatabaseId=${database.id};CollectionId=${container.id};${containerPermission._token}`;
try { try {
await page.goto(process.env.DATA_EXPLORER_ENDPOINT); await page.goto(process.env.DATA_EXPLORER_ENDPOINT);
await page.waitFor("div > p.switchConnectTypeText", { visible: true }); await page.waitFor("div > p.switchConnectTypeText", { visible: true });

View File

@ -127,8 +127,16 @@ const initTestExplorer = async (): Promise<void> => {
iframe.name = "explorer"; iframe.name = "explorer";
iframe.classList.add("iframe"); iframe.classList.add("iframe");
iframe.title = "explorer"; iframe.title = "explorer";
iframe.src = "explorer.html?platform=Portal&disablePortalInitCache"; iframe.src = getIframeSrc(selfServeType);
document.body.appendChild(iframe); document.body.appendChild(iframe);
}; };
const getIframeSrc = (selfServeType: string): string => {
let iframeSrc = "explorer.html?platform=Portal&disablePortalInitCache";
if (selfServeType) {
iframeSrc = `selfServe.html?selfServeType=${selfServeType}`;
}
return iframeSrc;
};
initTestExplorer(); initTestExplorer();

View File

@ -26,10 +26,14 @@ export function generateUniqueName(baseName = "", length = 4): string {
return `${baseName}${crypto.randomBytes(length).toString("hex")}`; return `${baseName}${crypto.randomBytes(length).toString("hex")}`;
} }
export async function createDatabase(frame: Frame) { export function generateDatabaseName(baseName = "db", length = 1): string {
const dbId = generateUniqueName("db"); return `${baseName}${crypto.randomBytes(length).toString("hex")}-${Date.now()}`;
}
export async function createDatabase(frame: Frame): Promise<string> {
const dbId = generateDatabaseName();
const collectionId = generateUniqueName("col"); const collectionId = generateUniqueName("col");
const shardKey = generateUniqueName(); const shardKey = "partitionKey";
// create new collection // create new collection
await frame.waitFor('button[data-test="New Collection"]', { visible: true }); await frame.waitFor('button[data-test="New Collection"]', { visible: true });
await frame.click('button[data-test="New Collection"]'); await frame.click('button[data-test="New Collection"]');
@ -63,9 +67,10 @@ export async function createDatabase(frame: Frame) {
// click submit // click submit
await frame.waitFor("#submitBtnAddCollection"); await frame.waitFor("#submitBtnAddCollection");
await frame.click("#submitBtnAddCollection"); await frame.click("#submitBtnAddCollection");
return dbId;
} }
export async function onClickSaveButton(frame: Frame) { export async function onClickSaveButton(frame: Frame): Promise<void> {
await frame.waitFor(`button[data-test="Save"]`), { visible: true }; await frame.waitFor(`button[data-test="Save"]`), { visible: true };
await frame.waitFor(LOADING_STATE_DELAY); await frame.waitFor(LOADING_STATE_DELAY);
await frame.click(`button[data-test="Save"]`); await frame.click(`button[data-test="Save"]`);

View File

@ -10,6 +10,7 @@
"./src/Contracts/ActionContracts.ts", "./src/Contracts/ActionContracts.ts",
"./src/Contracts/Diagnostics.ts", "./src/Contracts/Diagnostics.ts",
"./src/Contracts/ExplorerContracts.ts", "./src/Contracts/ExplorerContracts.ts",
"./src/Contracts/SelfServeContracts.ts",
"./src/Contracts/Versions.ts" "./src/Contracts/Versions.ts"
], ],
} }

View File

@ -1,51 +1,63 @@
const { CosmosClient } = require("@azure/cosmos"); const msRestNodeAuth = require("@azure/ms-rest-nodeauth");
const { CosmosDBManagementClient } = require("@azure/arm-cosmosdb");
const ms = require("ms");
const { time } = require("console");
// TODO: Add support for other API connection strings const clientId = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_ID"];
const mongoRegex = RegExp("mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com"); const secret = process.env["NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET"];
const tenantId = "72f988bf-86f1-41af-91ab-2d7cd011db47";
const subscriptionId = "69e02f2d-f059-4409-9eac-97e8a276ae2c";
const resourceGroupName = "runners";
const connectionString = process.env.PORTAL_RUNNER_CONNECTION_STRING; const sixtyMinutesAgo = new Date(Date.now() - 1000 * 60 * 60).getTime();
async function cleanup() { function friendlyTime(date) {
if (!connectionString) { try {
throw new Error("Connection string not provided"); return ms(date);
} catch (error) {
return "Unknown";
}
} }
let client; // Deletes all SQL and Mongo databases created more than 20 minutes ago in the test runner accounts
switch (true) { async function main() {
case connectionString.includes("mongodb://"): { const credentials = await msRestNodeAuth.loginWithServicePrincipalSecret(clientId, secret, tenantId);
const [, key, accountName] = connectionString.match(mongoRegex); const client = new CosmosDBManagementClient(credentials, subscriptionId);
client = new CosmosClient({ const accounts = await client.databaseAccounts.list(resourceGroupName);
key, for (const account of accounts) {
endpoint: `https://${accountName}.documents.azure.com:443/`, if (account.kind === "MongoDB") {
}); const mongoDatabases = await client.mongoDBResources.listMongoDBDatabases(resourceGroupName, account.name);
break; for (const database of mongoDatabases) {
} const timestamp = Number(database.name.split("-")[1]);
// TODO: Add support for other API connection strings if (timestamp && timestamp < sixtyMinutesAgo) {
default: await client.mongoDBResources.deleteMongoDBDatabase(resourceGroupName, account.name, database.name);
client = new CosmosClient(connectionString); console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
break;
}
const response = await client.databases.readAll().fetchAll();
return Promise.all(
response.resources.map(async (db) => {
const dbTimestamp = new Date(db._ts * 1000);
const twentyMinutesAgo = new Date(Date.now() - 1000 * 60 * 20);
if (dbTimestamp < twentyMinutesAgo) {
await client.database(db.id).delete();
console.log(`DELETED: ${db.id} | Timestamp: ${dbTimestamp}`);
} else { } else {
console.log(`SKIPPED: ${db.id} | Timestamp: ${dbTimestamp}`); console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
} else if (account.kind === "GlobalDocumentDB") {
const sqlDatabases = await client.sqlResources.listSqlDatabases(resourceGroupName, account.name);
for (const database of sqlDatabases) {
const timestamp = Number(database.name.split("-")[1]);
if (timestamp && timestamp < sixtyMinutesAgo) {
await client.sqlResources.deleteSqlDatabase(resourceGroupName, account.name, database.name);
console.log(`DELETED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
} else {
console.log(`SKIPPED: ${account.name} | ${database.name} | Age: ${friendlyTime(Date.now() - timestamp)}`);
}
}
}
} }
})
);
} }
cleanup() main()
.then(() => { .then(() => {
console.log("Completed");
process.exit(0); process.exit(0);
}) })
.catch((error) => { .catch((err) => {
console.error(error); console.log(err);
process.exit(1); console.log("Cleanup failed! Exiting with success. Cleanup should always fail safe.");
process.exit(0);
}); });

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