resolve merge master conflict

This commit is contained in:
hardiknai-techm
2021-03-31 17:08:50 +05:30
52 changed files with 1570 additions and 1508 deletions

View File

@@ -253,11 +253,8 @@ src/Terminal/NotebookAppContracts.d.ts
src/Terminal/index.ts
src/TokenProviders/PortalTokenProvider.ts
src/TokenProviders/TokenProviderFactory.ts
src/Utils/DatabaseAccountUtils.test.ts
src/Utils/DatabaseAccountUtils.ts
src/Utils/PricingUtils.test.ts
src/Utils/QueryUtils.test.ts
src/Utils/QueryUtils.ts
src/applyExplorerBindings.ts
src/global.d.ts
src/setupTests.ts

9
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"

940
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -44,9 +44,7 @@
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110",
"@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0",
"applicationinsights": "1.8.0",
"babel-polyfill": "6.26.0",
"bootstrap": "3.4.1",
"canvas": "file:./canvas",
"clean-webpack-plugin": "0.1.19",
@@ -61,8 +59,6 @@
"date-fns": "1.29.0",
"dayjs": "1.8.19",
"dotenv": "8.2.0",
"es6-object-assign": "1.1.0",
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2",
"eslint-plugin-react": "7.20.0",
"hasher": "1.2.0",
@@ -80,12 +76,9 @@
"monaco-editor": "0.18.1",
"ms": "2.1.3",
"msal": "1.4.4",
"object.entries": "1.1.0",
"office-ui-fabric-react": "7.164.2",
"p-retry": "4.2.0",
"plotly.js-cartesian-dist-min": "1.52.3",
"promise-polyfill": "8.1.0",
"promise.prototype.finally": "3.1.0",
"q": "1.5.1",
"react": "16.13.1",
"react-animate-height": "2.0.8",
@@ -102,13 +95,9 @@
"rxjs": "6.6.3",
"styled-components": "4.3.2",
"swr": "0.4.0",
"text-encoding": "0.7.0",
"terser-webpack-plugin": "3.1.0",
"underscore": "1.9.1",
"url-polyfill": "1.1.7",
"utility-types": "3.10.0",
"webcrypto-liner": "1.1.4",
"webfontloader": "1.6.28",
"whatwg-fetch": "3.0.0"
"utility-types": "3.10.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
@@ -138,9 +127,7 @@
"@types/react-redux": "7.1.7",
"@types/sinon": "2.3.3",
"@types/styled-components": "5.1.1",
"@types/text-encoding": "0.0.33",
"@types/underscore": "1.7.36",
"@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "4.0.1",
"@typescript-eslint/parser": "4.0.1",
"axe-puppeteer": "1.1.0",
@@ -165,7 +152,6 @@
"html-loader": "0.5.5",
"html-loader-jest": "0.2.1",
"html-webpack-plugin": "3.2.0",
"inline-css": "2.2.5",
"jest": "25.5.4",
"jest-canvas-mock": "2.1.0",
"jest-puppeteer": "4.4.0",
@@ -182,7 +168,6 @@
"rimraf": "3.0.0",
"sinon": "3.2.1",
"style-loader": "0.23.0",
"terser-webpack-plugin": "3.0.5",
"ts-loader": "6.2.2",
"tslint": "5.11.0",
"tslint-microsoft-contrib": "6.0.0",

View File

@@ -6,7 +6,7 @@ import Explorer from "../Explorer/Explorer";
import DocumentsTab from "../Explorer/Tabs/DocumentsTab";
import DocumentId from "../Explorer/Tree/DocumentId";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { QueryUtils } from "../Utils/QueryUtils";
import * as QueryUtils from "../Utils/QueryUtils";
import { BackendDefaults, HttpStatusCodes, SavedQueries } from "./Constants";
import { userContext } from "../UserContext";
import { queryDocumentsPage } from "./dataAccess/queryDocumentsPage";

View File

@@ -32,7 +32,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_isSystemDatabasePredicate": [Function],
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -974,7 +973,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"nonSystemDatabases": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -1228,7 +1226,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_isSystemDatabasePredicate": [Function],
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -2170,7 +2167,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"nonSystemDatabases": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -2437,7 +2433,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_isSystemDatabasePredicate": [Function],
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -3379,7 +3374,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"nonSystemDatabases": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],
@@ -3633,7 +3627,6 @@ exports[`SettingsComponent renders 1`] = `
"_closeSynapseLinkModalDialog": [Function],
"_isAfecFeatureRegistered": [Function],
"_isInitializingNotebooks": false,
"_isSystemDatabasePredicate": [Function],
"_panes": Array [
AddDatabasePane {
"autoPilotUsageCost": [Function],
@@ -4575,7 +4568,6 @@ exports[`SettingsComponent renders 1`] = `
"title": [Function],
"visible": [Function],
},
"nonSystemDatabases": [Function],
"notebookBasePath": [Function],
"notebookServerInfo": [Function],
"onRefreshDatabasesKeyPress": [Function],

View File

@@ -1,7 +1,7 @@
import React from "react";
import { shallow } from "enzyme";
import React from "react";
import { DescriptionType, NumberUiType, SmartUiInput } from "../../../SelfServe/SelfServeTypes";
import { SmartUiComponent, SmartUiDescriptor } from "./SmartUiComponent";
import { NumberUiType, SmartUiInput, DescriptionType } from "../../../SelfServe/SelfServeTypes";
describe("SmartUiComponent", () => {
const exampleData: SmartUiDescriptor = {
@@ -97,9 +97,9 @@ describe("SmartUiComponent", () => {
dataFieldName: "database",
type: "object",
choices: [
{ label: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" },
{ labelTKey: "Database 1", key: "db1" },
{ labelTKey: "Database 2", key: "db2" },
{ labelTKey: "Database 3", key: "db3" },
],
defaultKey: "db2",
},

View File

@@ -334,7 +334,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
dropdownWidth="auto"
options={choices.map((c) => ({
key: c.key,
text: this.props.getTranslation(c.label),
text: this.props.getTranslation(c.labelTKey),
}))}
styles={{
root: { width: 400 },

View File

@@ -2,17 +2,17 @@ jest.mock("../Graph/GraphExplorerComponent/GremlinClient");
jest.mock("../../Common/dataAccess/createCollection");
jest.mock("../../Common/dataAccess/createDocument");
import * as ko from "knockout";
import * as ViewModels from "../../Contracts/ViewModels";
import Q from "q";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { createDocument } from "../../Common/dataAccess/createDocument";
import Explorer from "../Explorer";
import * as ViewModels from "../../Contracts/ViewModels";
import { updateUserContext } from "../../UserContext";
import Explorer from "../Explorer";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
describe("ContainerSampleGenerator", () => {
const createExplorerStub = (database: ViewModels.Database): Explorer => {
const explorerStub = {} as Explorer;
explorerStub.nonSystemDatabases = ko.computed(() => [database]);
explorerStub.databases = ko.observableArray<ViewModels.Database>([database]);
explorerStub.isPreferredApiGraph = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiMongoDB = ko.computed<boolean>(() => false);
explorerStub.isPreferredApiDocumentDB = ko.computed<boolean>(() => false);

View File

@@ -1,9 +1,9 @@
import { DataSamplesUtil } from "./DataSamplesUtil";
import * as sinon from "sinon";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as ko from "knockout";
import * as sinon from "sinon";
import { Collection, Database } from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import { Database, Collection } from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import { DataSamplesUtil } from "./DataSamplesUtil";
describe("DataSampleUtils", () => {
const sampleCollectionId = "sampleCollectionId";
@@ -16,7 +16,7 @@ describe("DataSampleUtils", () => {
collections: ko.observableArray<Collection>([collection]),
} as Database;
const explorer = {} as Explorer;
explorer.nonSystemDatabases = ko.computed(() => [database]);
explorer.databases = ko.observableArray<Database>([database]);
explorer.showOkModalDialog = () => {};
const dataSamplesUtil = new DataSamplesUtil(explorer);

View File

@@ -1,8 +1,8 @@
import * as ViewModels from "../../Contracts/ViewModels";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import Explorer from "../Explorer";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContainerSampleGenerator } from "./ContainerSampleGenerator";
export class DataSamplesUtil {
private static readonly DialogTitle = "Create Sample Container";
@@ -17,7 +17,7 @@ export class DataSamplesUtil {
const databaseName = generator.getDatabaseId();
const containerName = generator.getCollectionId();
if (this.hasContainer(databaseName, containerName, this.container.nonSystemDatabases())) {
if (this.hasContainer(databaseName, containerName, this.container.databases())) {
const msg = `The container ${containerName} in database ${databaseName} already exists. Please delete it and retry.`;
this.container.showOkModalDialog(DataSamplesUtil.DialogTitle, msg);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);

View File

@@ -180,7 +180,6 @@ export default class Explorer {
// Resource Tree
public databases: ko.ObservableArray<ViewModels.Database>;
public nonSystemDatabases: ko.Computed<ViewModels.Database[]>;
public selectedDatabaseId: ko.Computed<string>;
public selectedCollectionId: ko.Computed<string>;
public isLeftPaneExpanded: ko.Observable<boolean>;
@@ -256,7 +255,6 @@ export default class Explorer {
public closeDialog: ExplorerParams["closeDialog"];
private _panes: ContextualPaneBase[] = [];
private _isSystemDatabasePredicate: (database: ViewModels.Database) => boolean = (database) => false;
private _isInitializingNotebooks: boolean;
private notebookBasePath: ko.Observable<string>;
private _arcadiaManager: ArcadiaResourceManager;
@@ -527,17 +525,6 @@ export default class Explorer {
configContext.platform === Platform.Portal && !this.isRunningOnNationalCloud() && !this.isPreferredApiGraph()
);
this.isRightPanelV2Enabled = ko.computed<boolean>(() => userContext.features.enableRightPanelV2);
this.defaultExperience.subscribe((defaultExperience: string) => {
if (
defaultExperience &&
defaultExperience.toLowerCase() === Constants.DefaultAccountExperience.Cassandra.toLowerCase()
) {
this._isSystemDatabasePredicate = (database: ViewModels.Database): boolean => {
return database.id() === "system";
};
}
});
this.selectedDatabaseId = ko.computed<string>(() => {
const selectedNode = this.selectedNode();
if (!selectedNode) {
@@ -559,10 +546,6 @@ export default class Explorer {
}
});
this.nonSystemDatabases = ko.computed(() => {
return this.databases().filter((database: ViewModels.Database) => !this._isSystemDatabasePredicate(database));
});
this.addCollectionPane = new AddCollectionPane({
isPreferredApiTable: ko.computed(() => this.isPreferredApiTable()),
id: "addcollectionpane",

View File

@@ -4,11 +4,8 @@
* - inspired from gremlin-javascript for nodejs: https://github.com/jbmusso/gremlin-javascript
* - tested on cosmosdb gremlin server
* - only supports sessionless gremlin requests
* - Relies on text-encoding polyfill (github.com/inexorabletash/text-encoding) for TextEncoder/TextDecoder on IE, Edge.
*/
import { TextEncoder, TextDecoder } from "text-encoding";
export interface GremlinSimpleClientParameters {
endpoint: string; // The websocket endpoint
user: string;

View File

@@ -114,7 +114,7 @@
aria-label="Keyspace id"
/>
<datalist id="keyspacesList" data-bind="foreach: container.nonSystemDatabases">
<datalist id="keyspacesList" data-bind="foreach: container.databases">
<option data-bind="value: $data.id"></option>
</datalist>

View File

@@ -261,10 +261,8 @@ export default class CassandraAddCollectionPane extends ContextualPaneBase {
});
this.keyspaceIds(cachedKeyspaceIdsList);
};
this.container.nonSystemDatabases.subscribe((newDatabases: ViewModels.Database[]) =>
updateKeyspaceIds(newDatabases)
);
updateKeyspaceIds(this.container.nonSystemDatabases());
this.container.databases.subscribe((newDatabases: ViewModels.Database[]) => updateKeyspaceIds(newDatabases));
updateKeyspaceIds(this.container.databases());
}
this.autoPilotUsageCost = ko.pureComputed<string>(() => {

View File

@@ -11,7 +11,7 @@ import editable from "../../Common/EditableUtility";
import * as HeadersUtility from "../../Common/HeadersUtility";
import TabsBase from "./TabsBase";
import { DocumentsGridMetrics } from "../../Common/Constants";
import { QueryUtils } from "../../Utils/QueryUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import NewDocumentIcon from "../../../images/NewDocument.svg";

View File

@@ -1,120 +0,0 @@
<div
class="tab-pane"
data-bind="
attr:{
id: tabId
}"
role="tabpanel"
>
<!-- Query Tab Command Bar - Start -->
<div class="contentdiv">
<div class="tabCommandButton">
<!-- Execute Query - Start -->
<span
class="commandButton"
data-bind="
click: onExecuteQueryClick,
visible: executeQueryButton.visible() && executeQueryButton.enabled()"
>
<img class="imgiconwidth" src="/ExecuteQuery.svg" />Run
</span>
<span
class="commandButton tabCommandDisabled"
data-bind="
visible: executeQueryButton.visible() && !executeQueryButton.enabled()"
>
<img class="imgiconwidth" src="/ExecuteQuery-disabled.svg" />Run
</span>
<!-- Execute Query - End -->
</div>
</div>
<!-- Query Tab Command Bar - End -->
<div
class="queryEditor"
data-bind="
attr: {
id: queryEditorId
},
css: {
mongoQueryEditor:$root.isPreferredApiMongoDB()
}"
></div>
<div
style="margin-left: 50px; margin-top: -75px"
data-bind="
visible: $root.isPreferredApiMongoDB() && sqlQueryEditorContent().length == 0"
>
Start by writing a Mongo query, for example: <strong>{'id':'foo'}</strong> or <strong>{ }</strong> to get all the
documents.
</div>
<!-- Query Errors Tab - Start-->
<div class="active queryErrorsHeaderContainer" data-bind="visible: errors().length > 0">
<span
class="queryErrors"
data-toggle="tab"
data-bind="
attr: {
href: '#queryerrors' + tabId
}"
>Errors</span
>
</div>
<!-- Query Errors Tab - End -->
<!-- Query Results & Errors Content Container - Start-->
<div class="queryResultErrorContentContainer">
<!-- Query Results Content - Start-->
<div
class="tab-pane active"
data-bind="
id: {
href: 'queryresults' + tabId
},
visible: allResultsMetadata().length > 0 && !errors().length > 0"
>
<div class="queryResultsValue">
<span class="queryResults"> Results: </span> <span data-bind="text: showingDocumentsDisplayText"></span>
<span class="queryResultDivider"> | </span> <span> Request Charge: </span>
<span data-bind="text: requestChargeDisplayText"></span> <span class="queryResultDivider"> | </span>
<span class="queryResultNextEnable" data-bind="visible: fetchNextPageButton.enabled">
<a
data-bind="
click: onFetchNextPageClick"
>
<span>Next</span> <img class="queryResultnextImg" src="/Query-Editor-Next.svg" />
</a>
</span>
<span class="queryResultNextDisable" data-bind="visible: !fetchNextPageButton.enabled()">
<span>Next</span> <img class="queryResultnextImg" src="/Query-Editor-Next-Disabled.svg" />
</span>
</div>
<div
style="height: 600px"
data-bind="
attr: {
id: resultsEditorId
}"
></div>
</div>
<!-- Query Results Content - Start-->
<!-- Query Errors Content - Start-->
<div
class="tab-pane active"
data-bind="
id: {
href: 'queryerrors' + tabId
},
visible: errors().length > 0"
>
<!-- ko foreach: errors -->
<div style="margin-left: 17px; font-size: 12px">
<span data-bind="text: $data.code"></span> : <span data-bind="text: $data.message"></span>
</div>
<!-- /ko -->
</div>
<!-- Query Errors Content - End-->
</div>
<!-- Results & Errors Content Container - Endt-->
</div>

View File

@@ -5,10 +5,8 @@ import QueryTab from "./QueryTab";
import * as HeadersUtility from "../../Common/HeadersUtility";
import { queryIterator } from "../../Common/MongoProxyClient";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";
import template from "./MongoQueryTab.html";
export default class MongoQueryTab extends QueryTab {
public static readonly component = { name: "mongo-query-tab", template };
public collection: ViewModels.Collection;
constructor(options: ViewModels.QueryTabOptions) {

View File

@@ -9,7 +9,7 @@ import * as HeadersUtility from "../../Common/HeadersUtility";
import { Splitter, SplitterBounds, SplitterDirection } from "../../Common/Splitter";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import ExecuteQueryIcon from "../../../images/ExecuteQuery.svg";
import { QueryUtils } from "../../Utils/QueryUtils";
import * as QueryUtils from "../../Utils/QueryUtils";
import SaveQueryIcon from "../../../images/save-cosmos.svg";
import { MinimalQueryIterator } from "../../Common/IteratorUtilities";

View File

@@ -76,10 +76,10 @@
<!-- Tabs Panes -- Start -->
<div class="tabPanesContainer">
<!-- ko if: activeTab && activeTab() -->
<div class="tabs-container" data-bind="visible: activeTab().isActive">
<!-- ko foreach: openedTabs -->
<div class="tabs-container" data-bind="visible: $data.isActive">
<span
data-bind="class: activeTab().constructor.component.name, component: { name: activeTab().constructor.component.name, params: activeTab }"
data-bind="class: $data.constructor.component.name, component: { name: $data.constructor.component.name, params: $data }"
></span>
</div>
<!-- /ko -->

View File

@@ -1,8 +1,8 @@
import Explorer from "../Explorer";
import * as ko from "knockout";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
import * as ViewModels from "../../Contracts/ViewModels";
import Explorer from "../Explorer";
import TabsBase from "../Tabs/TabsBase";
import { ResourceTreeAdapter } from "./ResourceTreeAdapter";
describe("ResourceTreeAdapter", () => {
const mockContainer = (): Explorer =>
@@ -18,7 +18,7 @@ describe("ResourceTreeAdapter", () => {
} as TabsBase),
},
isNotebookEnabled: ko.observable<boolean>(true),
nonSystemDatabases: ko.observable<ViewModels.Database[]>([]),
databases: ko.observable<ViewModels.Database[]>([]),
} as unknown) as Explorer);
// TODO isDataNodeSelected needs a better design and refactor, but for now, we protect some of the code paths

View File

@@ -1,39 +1,38 @@
import * as ko from "knockout";
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "office-ui-fabric-react";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem, TreeNodeComponent } from "../Controls/TreeComponent/TreeComponent";
import * as ViewModels from "../../Contracts/ViewModels";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import DeleteIcon from "../../../images/delete.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import _ from "underscore";
import { IPinnedRepo } from "../../Juno/JunoClient";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import GalleryIcon from "../../../images/GalleryIcon.svg";
import { Callout, Text, Link, DirectionalHint, Stack, ICalloutProps, ILinkProps } from "office-ui-fabric-react";
import FileIcon from "../../../images/notebook/file-cosmos.svg";
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import PublishIcon from "../../../images/notebook/publish_content.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { ArrayHashMap } from "../../Common/ArrayHashMap";
import { Areas } from "../../Common/Constants";
import * as DataModels from "../../Contracts/DataModels";
import * as ViewModels from "../../Contracts/ViewModels";
import { IPinnedRepo } from "../../Juno/JunoClient";
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../../UserContext";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
import Explorer from "../Explorer";
import UserDefinedFunction from "./UserDefinedFunction";
import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
import { NotebookUtil } from "../Notebook/NotebookUtil";
import TabsBase from "../Tabs/TabsBase";
import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger";
import TabsBase from "../Tabs/TabsBase";
import { userContext } from "../../UserContext";
import * as DataModels from "../../Contracts/DataModels";
import UserDefinedFunction from "./UserDefinedFunction";
export class ResourceTreeAdapter implements ReactAdapter {
public static readonly MyNotebooksTitle = "My Notebooks";
@@ -64,7 +63,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.koSubsCollectionIdMap = new ArrayHashMap();
this.databaseCollectionIdMap = new ArrayHashMap();
this.container.nonSystemDatabases.subscribe((databases: ViewModels.Database[]) => {
this.container.databases.subscribe((databases: ViewModels.Database[]) => {
// Clean up old databases
this.cleanupDatabasesKoSubs();
@@ -72,7 +71,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.triggerRender();
});
this.container.nonSystemDatabases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.container.databases().forEach((database: ViewModels.Database) => this.watchDatabase(database));
this.triggerRender();
}
@@ -190,7 +189,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
}
private buildDataTree(): TreeNode {
const databaseTreeNodes: TreeNode[] = this.container.nonSystemDatabases().map((database: ViewModels.Database) => {
const databaseTreeNodes: TreeNode[] = this.container.databases().map((database: ViewModels.Database) => {
const databaseNode: TreeNode = {
label: database.id(),
iconSrc: CosmosDBIcon,

View File

@@ -1,5 +0,0 @@
Number.isInteger =
Number.isInteger ||
function(value) {
return typeof value === "number" && isFinite(value) && Math.floor(value) === value;
};

View File

@@ -0,0 +1,5 @@
{
"Save": "Save",
"Discard": "Discard",
"Refresh": "Refesh"
}

View File

@@ -0,0 +1,31 @@
{
"NorthCentralUS": "North Central US",
"WestUS": "West US",
"EastUS2": "East US 2",
"Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.",
"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.",
"DecriptionLinkText": "Click here for more information",
"Regions": "Regions",
"RegionsPlaceholder": "Select a region",
"Enable Logging": "Enable Logging",
"Enable": "Enable",
"Disable": "Disable",
"Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable Database Level Throughput",
"Database Throughput": "Database Throughput",
"UpdateInProgressMessage": "Data is being updated",
"UpdateCompletedMessageTitle": "Update succeeded",
"UpdateCompletedMessageText": "Data update 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."
}

View File

@@ -0,0 +1,52 @@
{
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance.",
"DedicatedGateway": "Dedicated Gateway",
"Enable": "Enable",
"Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing.",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Dedicated gateway resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Dedicated gateway resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Dedicated gateway resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Dedicated gateway resource provisioning failed.",
"UpdateMessage": "Dedicated gateway resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Dedicated gateway resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Dedicated gateway resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Dedicated gateway resource updation failed.",
"DeleteMessage": "Dedicated gateway resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Dedicated gateway resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Dedicated gateway resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Dedicated gateway resource deletion failed.",
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment.",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "",
"SKUDetails": "SKU Details:",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
}

View File

@@ -1,92 +0,0 @@
{
"translations": {
"Common": {
"Save": "Save",
"Discard": "Discard",
"Refresh": "Refesh"
},
"SelfServeExample": {
"North Central US": "North Central US",
"West US": "West US",
"East US 2": "East US 2",
"Current Region": "Current Region",
"RegionDropdownInfo": "More regions can be added in the future.",
"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.",
"DecriptionLinkText": "Click here for more information",
"Regions": "Regions",
"RegionsPlaceholder": "Select a region",
"Enable Logging": "Enable Logging",
"Enable": "Enable",
"Disable": "Disable",
"Account Name": "Account Name",
"AccountNamePlaceHolder": "Enter the account name",
"Collection Throughput": "Collection Throughput",
"Enable DB level throughput": "Enable Database Level Throughput",
"Database Throughput": "Database Throughput",
"UpdateInProgressMessage": "Data is being updated",
"UpdateCompletedMessageTitle": "Update succeeded",
"UpdateCompletedMessageText": "Data update 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": {
"DedicatedGatewayDescription": "Provision a dedicated gateway cluster for your Azure Cosmos DB account. A dedicated gateway is compute that is a front-end to data in your Azure Cosmos DB account. Your dedicated gateway automatically includes the integrated cache, which can improve read performance. ",
"DedicatedGateway": "Dedicated Gateway",
"Enable": "Enable",
"Disable": "Disable",
"LearnAboutDedicatedGateway": "Learn more about dedicated gateway.",
"DeprovisioningDetailsText": "Learn more about deprovisioning the dedicated gateway.",
"DedicatedGatewayPricing": "Learn more about dedicated gateway pricing",
"SKUs": "SKUs",
"SKUsPlaceHolder": "Select SKUs",
"NumberOfInstances": "Number of instances",
"CosmosD4s": "Cosmos.D4s (General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory)",
"CosmosD8s": "Cosmos.D8s (General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory)",
"CosmosD16s": "Cosmos.D16s (General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory)",
"CosmosD32s": "Cosmos.D32s (General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory)",
"CreateMessage": "Dedicated gateway resource is being created.",
"CreateInitializeTitle": "Provisioning resource",
"CreateInitializeMessage": "Dedicated gateway resource will be provisioned.",
"CreateSuccessTitle": "Resource provisioned",
"CreateSuccesseMessage": "Dedicated gateway resource provisioned.",
"CreateFailureTitle": "Failed to provision resource",
"CreateFailureMessage": "Dedicated gateway resource provisioning failed.",
"UpdateMessage": "Dedicated gateway resource is being updated.",
"UpdateInitializeTitle": "Updating resource",
"UpdateInitializeMessage": "Dedicated gateway resource will be updated.",
"UpdateSuccessTitle": "Resource updated",
"UpdateSuccesseMessage": "Dedicated gateway resource updated.",
"UpdateFailureTitle": "Failed to update resource",
"UpdateFailureMessage": "Dedicated gateway resource updation failed.",
"DeleteMessage": "Dedicated gateway resource is being deleted.",
"DeleteInitializeTitle": "Deleting resource",
"DeleteInitializeMessage": "Dedicated gateway resource will be deleted.",
"DeleteSuccessTitle": "Resource deleted",
"DeleteSuccesseMessage": "Dedicated gateway resource deleted.",
"DeleteFailureTitle": "Failed to delete resource",
"DeleteFailureMessage": "Dedicated gateway resource deletion failed.",
"CannotSave": "Cannot save the changes to the Dedicated gateway resource at the moment",
"DedicatedGatewayEndpoint": "Dedicated gatewayEndpoint",
"NoValue": "",
"SKUDetails": "SKU Details: ",
"CosmosD4Details": "General Purpose Cosmos Compute with 4 vCPUs, 16 GB Memory",
"CosmosD8Details": "General Purpose Cosmos Compute with 8 vCPUs, 32 GB Memory",
"CosmosD16Details": "General Purpose Cosmos Compute with 16 vCPUs, 64 GB Memory",
"CosmosD32Details": "General Purpose Cosmos Compute with 32 vCPUs, 128 GB Memory",
"Cost": "Cost",
"CostText": "Hourly cost of the dedicated gateway resource depends on the SKU selection, number of instances per region, and number of regions.",
"ConnectionString": "Connection String",
"ConnectionStringText": "To use the dedicated gateway, use the connection string shown in ",
"KeysBlade": "the keys blade",
"WarningBannerOnUpdate": "Adding or modifying dedicated gateway instances may affect your bill.",
"WarningBannerOnDelete": "After deprovisioning the dedicated gateway, you must update any applications using the old dedicated gateway connection string."
}
}
}

View File

@@ -1,18 +1,8 @@
// CSS Dependencies
import "abort-controller/polyfill";
import "babel-polyfill";
import "bootstrap/dist/css/bootstrap.css";
import "es6-object-assign/auto";
import "es6-symbol/implement";
import "object.entries/auto";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import "promise-polyfill/src/polyfill";
import "promise.prototype.finally/auto";
import React, { useState } from "react";
import ReactDOM from "react-dom";
import "url-polyfill/url-polyfill.min";
import "webcrypto-liner/build/webcrypto-liner.shim.min";
import "whatwg-fetch";
import "../externals/jquery-ui.min.css";
import "../externals/jquery-ui.min.js";
import "../externals/jquery-ui.structure.min.css";
@@ -64,7 +54,6 @@ import { useConfig } from "./hooks/useConfig";
import { useKnockoutExplorer } from "./hooks/useKnockoutExplorer";
import { useSidePanel } from "./hooks/useSidePanel";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import "./Libs/is-integer-polyfill";
import "./Libs/jquery";
import "./Shared/appInsights";
import { userContext } from "./UserContext";

View File

@@ -23,11 +23,18 @@ export type Features = {
readonly ttl90Days: boolean;
};
export function extractFeatures(params?: URLSearchParams): Features {
params = params || new URLSearchParams(window.location.search);
export function extractFeatures(given = new URLSearchParams()): Features {
const downcased = new URLSearchParams();
params.forEach((value, key) => downcased.append(key.toLocaleLowerCase(), value));
const get = (key: string) => downcased.get("feature." + key.toLocaleLowerCase()) ?? undefined;
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string) => downcased.get("feature." + key) ?? undefined;
try {
new URLSearchParams(window.parent.location.search).forEach(set);
} catch {
//
} finally {
given.forEach(set);
}
return {
canExceedMaximumValue: "true" === get("canexceedmaximumvalue"),

View File

@@ -23,9 +23,9 @@ import {
} from "./SelfServeExample.rp";
const regionDropdownItems: ChoiceItem[] = [
{ label: "North Central US", key: Regions.NorthCentralUS },
{ label: "West US", key: Regions.WestUS },
{ label: "East US 2", key: Regions.EastUS2 },
{ labelTKey: "NorthCentralUS", key: Regions.NorthCentralUS },
{ labelTKey: "WestUS", key: Regions.WestUS },
{ labelTKey: "EastUS2", key: Regions.EastUS2 },
];
const regionDropdownInfo: Info = {

View File

@@ -2,10 +2,12 @@ import { Spinner, SpinnerSize } from "office-ui-fabric-react";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import * as React from "react";
import ReactDOM from "react-dom";
import { withTranslation } from "react-i18next";
import { normalizeArmEndpoint } from "../Common/EnvironmentUtility";
import { sendReadyMessage } from "../Common/MessageHandler";
import { configContext, updateConfigContext } from "../ConfigContext";
import { SelfServeFrameInputs } from "../Contracts/ViewModels";
import i18n from "../i18n";
import { updateUserContext } from "../UserContext";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import "./SelfServe.less";
@@ -14,14 +16,35 @@ import { SelfServeDescriptor } from "./SelfServeTypes";
import { SelfServeType } from "./SelfServeUtils";
initializeIcons();
const loadTranslationFile = async (className: string): Promise<void> => {
const language = i18n.languages[0];
const fileName = `${className}.json`;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let translations: any;
try {
translations = await import(`../Localization/${language}/${fileName}`);
} catch (e) {
translations = await import(`../Localization/en/${fileName}`);
}
i18n.addResourceBundle(language, className, translations.default, true);
};
const loadTranslations = async (className: string): Promise<void> => {
await loadTranslationFile("Common");
await loadTranslationFile(className);
};
const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDescriptor> => {
switch (selfServeType) {
case SelfServeType.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
await loadTranslations(SelfServeExample.default.name);
return new SelfServeExample.default().toSelfServeDescriptor();
}
case SelfServeType.sqlx: {
const SqlX = await import(/* webpackChunkName: "SqlX" */ "./SqlX/SqlX");
await loadTranslations(SqlX.default.name);
return new SqlX.default().toSelfServeDescriptor();
}
default:
@@ -33,7 +56,8 @@ const renderComponent = (selfServeDescriptor: SelfServeDescriptor): JSX.Element
if (!selfServeDescriptor) {
return <h1>Invalid self serve type!</h1>;
}
return <SelfServeComponent descriptor={selfServeDescriptor} />;
const SelfServeComponentTranslated = withTranslation()(SelfServeComponent);
return <SelfServeComponentTranslated descriptor={selfServeDescriptor} />;
};
const renderSpinner = (): JSX.Element => {

View File

@@ -1,5 +1,5 @@
import React from "react";
import { shallow } from "enzyme";
import React from "react";
import { SelfServeComponent, SelfServeComponentState } from "./SelfServeComponent";
import { NumberUiType, OnSaveResult, SelfServeDescriptor, SmartUiInput } from "./SelfServeTypes";
@@ -87,9 +87,9 @@ describe("SelfServeComponent", () => {
dataFieldName: "database",
type: "object",
choices: [
{ label: "Database 1", key: "db1" },
{ label: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" },
{ labelTKey: "Database 1", key: "db1" },
{ labelTKey: "Database 2", key: "db2" },
{ labelTKey: "Database 3", key: "db3" },
],
defaultKey: "db2",
},
@@ -106,7 +106,9 @@ describe("SelfServeComponent", () => {
};
it("should render and honor save, discard, refresh actions", async () => {
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
const wrapper = shallow(
<SelfServeComponent descriptor={exampleData} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0));
expect(wrapper).toMatchSnapshot();
@@ -158,7 +160,9 @@ describe("SelfServeComponent", () => {
});
it("getResolvedValue", async () => {
const wrapper = shallow(<SelfServeComponent descriptor={exampleData} />);
const wrapper = shallow(
<SelfServeComponent descriptor={exampleData} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0));
const selfServeComponent = wrapper.instance() as SelfServeComponent;
@@ -179,7 +183,9 @@ describe("SelfServeComponent", () => {
it("message bar and spinner snapshots", async () => {
const newDescriptor = { ...exampleData, onRefresh: onRefreshIsUpdatingMock };
let wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
let wrapper = shallow(
<SelfServeComponent descriptor={newDescriptor} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0));
let selfServeComponent = wrapper.instance() as SelfServeComponent;
selfServeComponent.onSaveButtonClick();
@@ -187,7 +193,9 @@ describe("SelfServeComponent", () => {
expect(wrapper).toMatchSnapshot();
newDescriptor.onRefresh = onRefreshMock;
wrapper = shallow(<SelfServeComponent descriptor={newDescriptor} />);
wrapper = shallow(
<SelfServeComponent descriptor={newDescriptor} t={undefined} i18n={undefined} tReady={undefined} />
);
await new Promise((resolve) => setTimeout(resolve, 0));
selfServeComponent = wrapper.instance() as SelfServeComponent;
selfServeComponent.onSaveButtonClick();

View File

@@ -8,15 +8,15 @@ import {
Spinner,
SpinnerSize,
Stack,
Text,
} from "office-ui-fabric-react";
import promiseRetry, { AbortError } from "p-retry";
import React from "react";
import { Translation } from "react-i18next";
import { WithTranslation } from "react-i18next";
import * as _ from "underscore";
import { sendMessage } from "../Common/MessageHandler";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import "../i18n";
import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles";
import {
AnyDisplay,
@@ -57,7 +57,7 @@ interface PortalNotificationContent {
};
}
export interface SelfServeComponentProps {
export interface SelfServeComponentProps extends WithTranslation {
descriptor: SelfServeDescriptor;
}
@@ -108,6 +108,9 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
this.retryIntervalInMs = SelfServeComponent.defaultRetryIntervalInMs;
}
this.retryOptions = { forever: true, maxTimeout: this.retryIntervalInMs, minTimeout: this.retryIntervalInMs };
// translation function passed to SelfServeComponent
this.translationFunction = this.props.t;
}
private onError = (hasErrors: boolean): void => {
@@ -391,8 +394,8 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
return this.getTranslation(key, "Common");
};
private getTranslation = (messageKey: string, prefix = `${this.smartUiGeneratorClassName}`): string => {
const translationKey = `${prefix}.${messageKey}`;
private getTranslation = (messageKey: string, namespace = `${this.smartUiGeneratorClassName}`): string => {
const translationKey = `${namespace}:${messageKey}`;
const translation = this.translationFunction ? this.translationFunction(translationKey) : messageKey;
if (translation === translationKey) {
return messageKey;
@@ -441,53 +444,45 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
public render(): JSX.Element {
if (this.state.compileErrorMessage) {
return <MessageBar messageBarType={MessageBarType.error}>{this.state.compileErrorMessage}</MessageBar>;
return (
<MessageBar messageBarType={MessageBarType.error}>
<Text>{this.state.compileErrorMessage}</Text>
</MessageBar>
);
}
return (
<Translation>
{(translate) => {
if (!this.translationFunction) {
this.translationFunction = translate;
}
return (
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens}>
<Stack.Item>
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />
<Separator styles={separatorStyles} />
</Stack.Item>
{this.state.isInitializing ? (
<Spinner size={SpinnerSize.large} />
) : (
<>
{this.state.notification && (
<MessageBar
messageBarType={this.state.notification.type}
onDismiss={
this.state.notification.isCancellable
? () => this.setState({ notification: undefined })
: undefined
}
>
{this.state.notification.message}
</MessageBar>
)}
<SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
onError={this.onError}
getTranslation={this.getTranslation}
/>
</>
)}
</Stack>
</div>
);
}}
</Translation>
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens}>
<Stack.Item>
<CommandBar styles={commandBarStyles} items={this.getCommandBarItems()} />
<Separator styles={separatorStyles} />
</Stack.Item>
{this.state.isInitializing ? (
<Spinner size={SpinnerSize.large} />
) : (
<>
{this.state.notification && (
<MessageBar
messageBarType={this.state.notification.type}
onDismiss={
this.state.notification.isCancellable ? () => this.setState({ notification: undefined }) : undefined
}
>
<Text>{this.state.notification.message}</Text>
</MessageBar>
)}
<SmartUiComponent
disabled={this.state.refreshResult?.isUpdateInProgress || this.state.isSaving}
descriptor={this.state.root as SmartUiDescriptor}
currentValues={this.state.currentValues}
onInputChange={this.onInputChange}
onError={this.onError}
getTranslation={this.getTranslation}
/>
</>
)}
</Stack>
</div>
);
}
}

View File

@@ -0,0 +1,69 @@
import { sendMessage } from "../Common/MessageHandler";
import { configContext } from "../ConfigContext";
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
import { appInsights } from "../Shared/appInsights";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
import { userContext } from "../UserContext";
import { SelfServeTelemetryMessage } from "./SelfServeTypes";
const action = Action.SelfServe;
export const trace = (data: SelfServeTelemetryMessage): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Mark, data);
appInsights.trackEvent({ name: Action[action] }, decorateData(data, ActionModifiers.Mark));
};
export const traceStart = (data: SelfServeTelemetryMessage): number => {
const timestamp: number = Date.now();
sendSelfServeTelemetryMessage(ActionModifiers.Start, data);
appInsights.startTrackEvent(Action[action]);
return timestamp;
};
export const traceSuccess = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Success, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Success));
};
export const traceFailure = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Failed, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Failed));
};
export const traceCancel = (data: SelfServeTelemetryMessage, timestamp?: number): void => {
sendSelfServeTelemetryMessage(ActionModifiers.Cancel, data, timestamp || Date.now());
appInsights.stopTrackEvent(Action[action], decorateData(data, ActionModifiers.Cancel));
};
const sendSelfServeTelemetryMessage = (
actionModifier: string,
data: SelfServeTelemetryMessage,
timeStamp?: number
): void => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const dataToSend: any = {
type: SelfServeMessageTypes.TelemetryInfo,
data: {
action: Action[action],
actionModifier: actionModifier,
data: JSON.stringify(decorateData(data)),
},
};
if (timeStamp) {
dataToSend.data.timeStamp = timeStamp;
}
sendMessage(dataToSend);
};
const decorateData = (data: SelfServeTelemetryMessage, actionModifier?: string) => {
return {
databaseAccountName: userContext.databaseAccount?.name,
defaultExperience: userContext.defaultExperience,
authType: userContext.authType,
subscriptionId: userContext.subscriptionId,
platform: configContext.platform,
env: process.env.NODE_ENV,
actionModifier,
...data,
} as { [key: string]: string };
};

View File

@@ -98,7 +98,7 @@ export enum NumberUiType {
Slider = "Slider",
}
export type ChoiceItem = { label: string; key: string };
export type ChoiceItem = { labelTKey: string; key: string };
export type InputType = number | string | boolean | ChoiceItem | Description;
@@ -157,3 +157,9 @@ export interface RefreshResult {
export interface RefreshParams {
retryIntervalInMs: number;
}
export interface SelfServeTelemetryMessage {
selfServeClassName: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}

View File

@@ -131,9 +131,9 @@ describe("SelfServeUtils", () => {
type: "object",
labelTKey: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" },
{ labelTKey: "South West US", key: "SWUS" },
{ labelTKey: "North Central US", key: "NCUS" },
{ labelTKey: "East US 2", key: "EUS2" },
],
},
],
@@ -238,9 +238,9 @@ describe("SelfServeUtils", () => {
type: "object",
labelTKey: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" },
{ labelTKey: "South West US", key: "SWUS" },
{ labelTKey: "North Central US", key: "NCUS" },
{ labelTKey: "East US 2", key: "EUS2" },
],
},
children: [] as Node[],

View File

@@ -195,5 +195,5 @@ export const generateBladeLink = (blade: BladeType): string => {
const subscriptionId = userContext.subscriptionId;
const resourceGroupName = userContext.resourceGroup;
const databaseAccountName = userContext.databaseAccount.name;
return `https://portal.azure.com/#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
return `${document.referrer}#@microsoft.onmicrosoft.com/resource/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDb/databaseAccounts/${databaseAccountName}/${blade}`;
};

View File

@@ -1,4 +1,5 @@
import { IsDisplayable, OnChange, RefreshOptions, Values } from "../Decorators";
import { trace } from "../SelfServeTelemetryProcessor";
import {
ChoiceItem,
Description,
@@ -147,10 +148,10 @@ const onEnableDedicatedGatewayChange = (
};
const skuDropDownItems: ChoiceItem[] = [
{ label: "CosmosD4s", key: CosmosD4s },
{ label: "CosmosD8s", key: CosmosD8s },
{ label: "CosmosD16s", key: CosmosD16s },
{ label: "CosmosD32s", key: CosmosD32s },
{ labelTKey: "CosmosD4s", key: CosmosD4s },
{ labelTKey: "CosmosD8s", key: CosmosD8s },
{ labelTKey: "CosmosD16s", key: CosmosD16s },
{ labelTKey: "CosmosD32s", key: CosmosD32s },
];
const getSkus = async (): Promise<ChoiceItem[]> => {
@@ -176,6 +177,8 @@ export default class SqlX extends SelfServeBaseClass {
currentValues: Map<string, SmartUiInput>,
baselineValues: Map<string, SmartUiInput>
): Promise<OnSaveResult> => {
trace({ selfServeClassName: "SqlX" });
const dedicatedGatewayCurrentlyEnabled = currentValues.get("enableDedicatedGateway")?.value as boolean;
const dedicatedGatewayOriginallyEnabled = baselineValues.get("enableDedicatedGateway")?.value as boolean;

File diff suppressed because it is too large Load Diff

View File

@@ -114,6 +114,7 @@ export enum Action {
NotebooksGalleryPublicGalleryCount,
NotebooksGalleryFavoritesCount,
NotebooksGalleryPublishedCount,
SelfServe,
}
export const ActionModifiers = {

View File

@@ -1,58 +1,11 @@
import { Action, ActionModifiers } from "./TelemetryConstants";
import { sendMessage } from "../../Common/MessageHandler";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { appInsights } from "../appInsights";
import { sendMessage } from "../../Common/MessageHandler";
import { configContext } from "../../ConfigContext";
import { MessageTypes } from "../../Contracts/ExplorerContracts";
import { userContext } from "../../UserContext";
import { appInsights } from "../appInsights";
import { Action, ActionModifiers } from "./TelemetryConstants";
// TODO: Remove this. It is perfectly find to pass any data to telemtry methods.
// This was added only to maintain stability while removing dependencies on explorer.databaseAccount and explorer.defaultExperience
type allowedKeys =
| "notebookId"
| "collectionId"
| "collectionName"
| "error"
| "isSample"
| "downloadCount"
| "baseUrl"
| "source"
| "description"
| "dataExplorerArea"
| "databaseName"
| "downloadCount"
| "favoriteCount"
| "abuseCategory"
| "errorStack"
| "tabTitle"
| "tabId"
| "conflictResourceType"
| "conflictOperationType"
| "conflictResourceId"
| "message"
| "files"
| "notebooks"
| "directories"
| "tabName"
| "databaseId"
| "queryName"
| "isPublishPending"
| "label"
| "scopesSelected"
| "title"
| "level"
| "changedSelectedValueTo"
| "area"
| "area"
| "paneTitle"
| "notebookUrl"
| "isNotebookEnabled"
| "commandButtonClicked"
| "count"
| "publishedCount"
| "underReviewCount"
| "removedCount";
type TelemetryData = { [key in allowedKeys]?: unknown };
type TelemetryData = { [key: string]: unknown };
export function trace(action: Action, actionModifier: string = ActionModifiers.Mark, data: TelemetryData = {}): void {
sendMessage({

View File

@@ -1,5 +1,3 @@
import "babel-polyfill";
import "promise-polyfill/src/polyfill"; // polyfill Promise on IE
import "@jupyterlab/terminal/style/index.css";
import "./index.css";
import { ServerConnection } from "@jupyterlab/services";

View File

@@ -2,7 +2,7 @@ import * as DataModels from "../Contracts/DataModels";
import * as Q from "q";
import * as sinon from "sinon";
import * as ViewModels from "../Contracts/ViewModels";
import { QueryUtils } from "./QueryUtils";
import * as QueryUtils from "./QueryUtils";
describe("Query Utils", () => {
function generatePartitionKeyForPath(path: string): DataModels.PartitionKey {

View File

@@ -1,118 +1,114 @@
import Q from "q";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
export class QueryUtils {
public static buildDocumentsQuery(
filter: string,
partitionKeyProperty: string,
partitionKey: DataModels.PartitionKey
): string {
let query: string = partitionKeyProperty
? `select c.id, c._self, c._rid, c._ts, ${QueryUtils.buildDocumentsQueryPartitionProjections(
"c",
partitionKey
)} as _partitionKeyValue from c`
: `select c.id, c._self, c._rid, c._ts from c`;
export function buildDocumentsQuery(
filter: string,
partitionKeyProperty: string,
partitionKey: DataModels.PartitionKey
): string {
let query = partitionKeyProperty
? `select c.id, c._self, c._rid, c._ts, ${buildDocumentsQueryPartitionProjections(
"c",
partitionKey
)} as _partitionKeyValue from c`
: `select c.id, c._self, c._rid, c._ts from c`;
if (filter) {
query += " " + filter;
}
return query;
if (filter) {
query += " " + filter;
}
public static buildDocumentsQueryPartitionProjections(
collectionAlias: string,
partitionKey: DataModels.PartitionKey
): string {
if (!partitionKey) {
return "";
}
// e.g., path /order/id will be projected as c["order"]["id"],
// to escape any property names that match a keyword
let projections = [];
for (let index in partitionKey.paths) {
// TODO: Handle "/" in partition key definitions
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
let projectedProperty: string = "";
projectedProperties.forEach((property: string) => {
const projection = property.trim();
if (projection.length > 0 && projection.charAt(0) != "'" && projection.charAt(0) != '"') {
projectedProperty = projectedProperty + `["${projection}"]`;
} else if (projection.length > 0 && projection.charAt(0) == "'") {
// trim single quotes and escape double quotes
const projectionSlice = projection.slice(1, projection.length - 1);
projectedProperty =
projectedProperty + `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`;
} else {
projectedProperty = projectedProperty + `[${projection}]`;
}
});
projections.push(`${collectionAlias}${projectedProperty}`);
}
return projections.join(",");
}
public static async queryPagesUntilContentPresent(
firstItemIndex: number,
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Promise<ViewModels.QueryResults> {
let roundTrips: number = 0;
let netRequestCharge: number = 0;
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
const results: ViewModels.QueryResults = await queryItems(itemIndex);
roundTrips = roundTrips + 1;
results.roundTrips = roundTrips;
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
netRequestCharge = Number(results.requestCharge);
const resultsMetadata: ViewModels.QueryResultsMetadata = {
hasMoreResults: results.hasMoreResults,
itemCount: results.itemCount,
firstItemIndex: results.firstItemIndex,
lastItemIndex: results.lastItemIndex,
};
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
return await doRequest(resultsMetadata.lastItemIndex);
}
return results;
};
return await doRequest(firstItemIndex);
}
public static async queryAllPages(
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Promise<ViewModels.QueryResults> {
const queryResults: ViewModels.QueryResults = {
documents: [],
activityId: undefined,
hasMoreResults: false,
itemCount: 0,
firstItemIndex: 0,
lastItemIndex: 0,
requestCharge: 0,
roundTrips: 0,
};
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
const results: ViewModels.QueryResults = await queryItems(itemIndex);
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
queryResults.roundTrips = queryResults.roundTrips + 1;
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
queryResults.hasMoreResults = hasMoreResults;
queryResults.itemCount = queryResults.itemCount + itemCount;
queryResults.lastItemIndex = lastItemIndex;
queryResults.documents = queryResults.documents.concat(documents);
if (queryResults.hasMoreResults) {
return doRequest(queryResults.lastItemIndex + 1);
}
return queryResults;
};
return doRequest(0);
}
return query;
}
export function buildDocumentsQueryPartitionProjections(
collectionAlias: string,
partitionKey?: DataModels.PartitionKey
): string {
if (!partitionKey) {
return "";
}
// e.g., path /order/id will be projected as c["order"]["id"],
// to escape any property names that match a keyword
const projections = [];
for (const index in partitionKey.paths) {
// TODO: Handle "/" in partition key definitions
const projectedProperties: string[] = partitionKey.paths[index].split("/").slice(1);
let projectedProperty = "";
projectedProperties.forEach((property: string) => {
const projection = property.trim();
if (projection.length > 0 && projection.charAt(0) !== "'" && projection.charAt(0) !== '"') {
projectedProperty += `["${projection}"]`;
} else if (projection.length > 0 && projection.charAt(0) === "'") {
// trim single quotes and escape double quotes
const projectionSlice = projection.slice(1, projection.length - 1);
projectedProperty += `["${projectionSlice.replace(/\\"/g, '"').replace(/"/g, '\\\\\\"')}"]`;
} else {
projectedProperty += `[${projection}]`;
}
});
projections.push(`${collectionAlias}${projectedProperty}`);
}
return projections.join(",");
}
export const queryPagesUntilContentPresent = async (
firstItemIndex: number,
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Promise<ViewModels.QueryResults> => {
let roundTrips = 0;
let netRequestCharge = 0;
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
const results = await queryItems(itemIndex);
roundTrips = roundTrips + 1;
results.roundTrips = roundTrips;
results.requestCharge = Number(results.requestCharge) + netRequestCharge;
netRequestCharge = Number(results.requestCharge);
const resultsMetadata = {
hasMoreResults: results.hasMoreResults,
itemCount: results.itemCount,
firstItemIndex: results.firstItemIndex,
lastItemIndex: results.lastItemIndex,
};
if (resultsMetadata.itemCount === 0 && resultsMetadata.hasMoreResults) {
return await doRequest(resultsMetadata.lastItemIndex);
}
return results;
};
return await doRequest(firstItemIndex);
};
export const queryAllPages = async (
queryItems: (itemIndex: number) => Promise<ViewModels.QueryResults>
): Promise<ViewModels.QueryResults> => {
const queryResults: ViewModels.QueryResults = {
documents: [],
activityId: undefined,
hasMoreResults: false,
itemCount: 0,
firstItemIndex: 0,
lastItemIndex: 0,
requestCharge: 0,
roundTrips: 0,
};
const doRequest = async (itemIndex: number): Promise<ViewModels.QueryResults> => {
const results = await queryItems(itemIndex);
const { requestCharge, hasMoreResults, itemCount, lastItemIndex, documents } = results;
queryResults.roundTrips = queryResults.roundTrips + 1;
queryResults.requestCharge = Number(queryResults.requestCharge) + Number(requestCharge);
queryResults.hasMoreResults = hasMoreResults;
queryResults.itemCount = queryResults.itemCount + itemCount;
queryResults.lastItemIndex = lastItemIndex;
queryResults.documents = queryResults.documents.concat(documents);
if (queryResults.hasMoreResults) {
return doRequest(queryResults.lastItemIndex + 1);
}
return queryResults;
};
return doRequest(0);
};

View File

@@ -260,7 +260,7 @@ async function configurePortal(explorerParams: ExplorerParams): Promise<Explorer
explorer.configure(inputs);
resolve(explorer);
if (openAction) {
handleOpenAction(openAction, explorer.nonSystemDatabases(), explorer);
handleOpenAction(openAction, explorer.databases(), explorer);
}
}
},

View File

@@ -1,22 +1,14 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import XHR from "i18next-http-backend";
import EnglishTranslations from "./Localization/en/translations.json";
i18n
.use(XHR)
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
en: EnglishTranslations,
},
fallbackLng: "en",
detection: { order: ["navigator", "cookie", "localStorage", "sessionStorage", "querystring", "htmlTag"] },
debug: process.env.NODE_ENV === "development",
ns: ["translations"],
defaultNS: "translations",
keySeparator: ".",
interpolation: {
formatSeparator: ",",
@@ -29,3 +21,5 @@ i18n
useSuspense: false,
},
});
export default i18n;

View File

@@ -1,7 +1,8 @@
import Adapter from "enzyme-adapter-react-16";
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import "jest-canvas-mock";
import { initializeIcons } from "office-ui-fabric-react";
import { TextDecoder, TextEncoder } from "util";
configure({ adapter: new Adapter() });
initializeIcons();
@@ -13,3 +14,5 @@ if (typeof window.URL.createObjectURL === "undefined") {
(<any>window).$ = (<any>window).jQuery = require("jquery");
(<any>global).$ = (<any>global).$.jQuery = require("jquery");
require("jquery-ui-dist/jquery-ui");
(<any>global).TextEncoder = TextEncoder;
(<any>global).TextDecoder = TextDecoder;

View File

@@ -1,7 +1,6 @@
import "babel-polyfill";
import { DocumentClientParams, UploadDetailsRecord, UploadDetails } from "./definitions";
import { client } from "../../Common/CosmosClient";
import { configContext, updateConfigContext } from "../../ConfigContext";
import { updateConfigContext } from "../../ConfigContext";
import { updateUserContext } from "../../UserContext";
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";

View File

@@ -11,7 +11,7 @@
"allowSyntheticDefaultImports": true,
"downlevelIteration": true,
"module": "esnext",
"target": "es5",
"target": "es2017",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": ["es5", "es6", "dom", "webworker.importscripts"],

View File

@@ -51,9 +51,9 @@
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
},
"node-fetch": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw=="
},
"supports-color": {
"version": "7.1.0",

View File

@@ -12,6 +12,6 @@
"dependencies": {
"chalk": "^4.1.0",
"moment": "^2.27.0",
"node-fetch": "^2.6.0"
"node-fetch": "^2.6.1"
}
}

View File

@@ -81,23 +81,6 @@ const typescriptRule = {
exclude: /node_modules/,
};
// Third party modules are compiled with babel since using ts-loader that much causes webpack to run out of memory
const ModulesRule = {
test: /\.js$/,
use: [
{
loader: "babel-loader",
options: {
cacheDirectory: ".cache/babel",
presets: [["@babel/preset-env", { targets: { ie: "11" }, useBuiltIns: false }]],
},
},
],
include: /node_modules/,
// Exclude large modules we know don't need transpiling
exclude: /vega|monaco|plotly/,
};
module.exports = function (env = {}, argv = {}) {
const mode = argv.mode || "development";
const rules = [fontRule, lessRule, imagesRule, cssRule, htmlRule, typescriptRule];
@@ -107,7 +90,6 @@ module.exports = function (env = {}, argv = {}) {
};
if (mode === "production") {
rules.push(ModulesRule);
envVars.NODE_ENV = "production";
}
@@ -235,7 +217,6 @@ module.exports = function (env = {}, argv = {}) {
minimize: mode === "production" ? true : false,
minimizer: [
new TerserPlugin({
cache: ".cache/terser",
terserOptions: {
// These options increase our initial bundle size by ~5% but the builds are significantly faster and won't run out of memory
compress: false,