mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-23 19:01:28 +00:00
Compare commits
51 Commits
eslint/fix
...
eslint/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7660a11c44 | ||
|
|
1c54459708 | ||
|
|
df3b18d585 | ||
|
|
882f0e1554 | ||
|
|
b67b76cc87 | ||
|
|
734ee1e436 | ||
|
|
ff498b51e2 | ||
|
|
ed1ffb692f | ||
|
|
f7fa3f7c09 | ||
|
|
6ebf19c0c9 | ||
|
|
f968f57543 | ||
|
|
6ca8e3c6f4 | ||
|
|
f7e7240010 | ||
|
|
acc095a482 | ||
|
|
288e6410f3 | ||
|
|
9ac3392271 | ||
|
|
9a908dde9a | ||
|
|
ddd2e63fe7 | ||
|
|
34c59b4872 | ||
|
|
7d28af4fc7 | ||
|
|
50b99ceb42 | ||
|
|
15a26d6500 | ||
|
|
a8150af269 | ||
|
|
6a9a0156a3 | ||
|
|
ead28f043f | ||
|
|
b05e5a2145 | ||
|
|
5e8aa491ba | ||
|
|
a730c08292 | ||
|
|
3dce5cd129 | ||
|
|
7c186c3ef2 | ||
|
|
d8840a0dfd | ||
|
|
f853c4ec2f | ||
|
|
9bf5f48165 | ||
|
|
0b2a204b70 | ||
|
|
c28593b752 | ||
|
|
4cbbef9574 | ||
|
|
300c952a9b | ||
|
|
38c3761260 | ||
|
|
3032f689b6 | ||
|
|
8b30af3d9e | ||
|
|
e10240bd7a | ||
|
|
ae9c27795e | ||
|
|
d7997d716e | ||
|
|
af0dc3094b | ||
|
|
665270296f | ||
|
|
2d945c8231 | ||
|
|
8866976bb4 | ||
|
|
d10f3c69f1 | ||
|
|
7e4f030547 | ||
|
|
05f46dd635 | ||
|
|
65882ea831 |
@@ -87,11 +87,11 @@ src/Explorer/Tables/DataTable/TableCommands.ts
|
||||
src/Explorer/Tables/DataTable/TableEntityCache.ts
|
||||
src/Explorer/Tables/DataTable/TableEntityListViewModel.ts
|
||||
src/Explorer/Tables/Entities.ts
|
||||
src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
|
||||
src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
|
||||
# src/Explorer/Tables/QueryBuilder/ClauseGroup.ts
|
||||
# src/Explorer/Tables/QueryBuilder/ClauseGroupViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/CustomTimestampHelper.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
|
||||
src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
|
||||
# src/Explorer/Tables/QueryBuilder/QueryBuilderViewModel.ts
|
||||
# src/Explorer/Tables/QueryBuilder/QueryClauseViewModel.ts
|
||||
src/Explorer/Tables/TableDataClient.ts
|
||||
src/Explorer/Tables/TableEntityProcessor.ts
|
||||
src/Explorer/Tables/Utilities.ts
|
||||
@@ -113,18 +113,10 @@ src/Explorer/Tree/ConflictId.ts
|
||||
src/Explorer/Tree/DocumentId.ts
|
||||
src/Explorer/Tree/ObjectId.ts
|
||||
src/Explorer/Tree/ResourceTokenCollection.ts
|
||||
src/Explorer/Tree/StoredProcedure.ts
|
||||
src/Explorer/Tree/TreeComponents.ts
|
||||
src/Explorer/Tree/Trigger.ts
|
||||
src/Explorer/WaitsForTemplateViewModel.ts
|
||||
src/GitHub/GitHubClient.test.ts
|
||||
src/GitHub/GitHubClient.ts
|
||||
src/GitHub/GitHubConnector.ts
|
||||
src/GitHub/GitHubOAuthService.ts
|
||||
src/Index.ts
|
||||
src/Juno/JunoClient.test.ts
|
||||
src/Juno/JunoClient.ts
|
||||
src/Platform/Hosted/Authorization.ts
|
||||
src/ReactDevTools.ts
|
||||
src/Shared/Constants.ts
|
||||
src/Shared/DefaultExperienceUtility.test.ts
|
||||
@@ -143,7 +135,7 @@ src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
|
||||
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
|
||||
src/Explorer/Controls/TreeComponent/TreeComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.test.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
||||
; src/Explorer/Graph/GraphExplorerComponent/GraphExplorer.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/GraphVizComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/LeftPaneComponent.tsx
|
||||
src/Explorer/Graph/GraphExplorerComponent/MiddlePaneComponent.tsx
|
||||
@@ -153,10 +145,10 @@ src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.test.t
|
||||
src/Explorer/Graph/GraphExplorerComponent/ReadOnlyNodePropertiesComponent.tsx
|
||||
src/Explorer/Menus/CommandBar/CommandBarUtil.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentAdapter.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
||||
; src/Explorer/Notebook/NotebookComponent/NotebookComponentBootstrapper.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/VirtualCommandBarComponent.tsx
|
||||
src/Explorer/Notebook/NotebookComponent/contents/index.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
||||
; src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/NotebookRenderer.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/decorators/draggable/index.tsx
|
||||
src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/index.tsx
|
||||
|
||||
@@ -39,7 +39,6 @@ module.exports = {
|
||||
"@typescript-eslint/switch-exhaustiveness-check": "error",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"no-null/no-null": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"prefer-arrow/prefer-arrow-functions": ["error", { allowStandaloneDeclarations: true }],
|
||||
eqeqeq: "error",
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
version="1.1"
|
||||
viewBox="0 0 16 16"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path
|
||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||
transform="scale(0.5)"
|
||||
fill="#000"
|
||||
stroke="#CCC"
|
||||
>
|
||||
<path
|
||||
d="M14,2.691l-5.301,5.309l5.301,5.309l-0.691,0.691l-5.309,-5.301l-5.309,5.301l-0.691,-0.691l5.301,-5.309l-5.301,-5.309l0.691,-0.691l5.309,5.301l5.309,-5.301l0.691,0.691Z"
|
||||
transform="scale(0.5)" fill="#000" stroke="#000">
|
||||
</path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 503 B After Width: | Height: | Size: 449 B |
@@ -37,8 +37,8 @@ module.exports = {
|
||||
global: {
|
||||
branches: 25,
|
||||
functions: 25,
|
||||
lines: 30,
|
||||
statements: 30,
|
||||
lines: 29,
|
||||
statements: 29,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -2357,6 +2357,8 @@ a:link {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -2832,6 +2834,8 @@ a:link {
|
||||
|
||||
#explorerNotificationConsole {
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.uniqueIndexesContainer {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface CollapsedResourceTreeProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@@ -11,6 +12,21 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
toggleLeftPaneExpanded,
|
||||
isLeftPaneExpanded,
|
||||
}: CollapsedResourceTreeProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div id="mini" className={!isLeftPaneExpanded ? "mini toggle-mini" : "hiddenMain"}>
|
||||
<div className="main-nav nav">
|
||||
@@ -21,11 +37,14 @@ export const CollapsedResourceTree: FunctionComponent<CollapsedResourceTreeProps
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Expand Tree"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
ref={focusButton}
|
||||
>
|
||||
<span className="leftarrowCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span className="leftarrowCollapsed">
|
||||
<img className="arrowCollapsed" src={arrowLeftImg} alt="Expand" />
|
||||
</span>
|
||||
<span className="collectionCollapsed" onClick={toggleLeftPaneExpanded}>
|
||||
<span className="collectionCollapsed">
|
||||
<span>{userContext.apiType} API</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -339,10 +339,11 @@ export enum ConflictOperationType {
|
||||
}
|
||||
|
||||
export enum ConnectionStatusType {
|
||||
Connect = "Connect",
|
||||
Connecting = "Connecting",
|
||||
Allocating = "Allocating",
|
||||
Connected = "Connected",
|
||||
Failed = "Connection Failed",
|
||||
ReConnect = "Reconnect",
|
||||
}
|
||||
|
||||
export const EmulatorMasterKey =
|
||||
@@ -354,15 +355,32 @@ export const StyleConstants = require("less-vars-loader!../../less/Common/Consta
|
||||
|
||||
export class Notebook {
|
||||
public static readonly defaultBasePath = "./notebooks";
|
||||
public static readonly heartbeatDelayMs = 5000;
|
||||
public static readonly heartbeatDelayMs = 60000;
|
||||
public static readonly kernelRestartInitialDelayMs = 1000;
|
||||
public static readonly kernelRestartMaxDelayMs = 20000;
|
||||
public static readonly autoSaveIntervalMs = 120000;
|
||||
public static readonly memoryGuageToGB = 1048576;
|
||||
public static readonly temporarilyDownMsg = "Notebooks is currently not available. We are working on it.";
|
||||
public static readonly mongoShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Mongo Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
public static readonly cassandraShellTemporarilyDownMsg =
|
||||
"We have identified an issue with the Cassandra Shell and it is unavailable right now. We are actively working on the mitigation.";
|
||||
public static saveNotebookModalTitle = "Save Notebook in temporary workspace";
|
||||
public static saveNotebookModalContent =
|
||||
"This notebook will be saved in the temporary workspace and will be removed when the session expires. To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends.";
|
||||
public static newNotebookModalTitle = "Create Notebook in temporary workspace";
|
||||
public static newNotebookUploadModalTitle = "Upload Notebook in temporary workspace";
|
||||
public static newNotebookModalContent1 =
|
||||
"A temporary workspace will be created to enable you to work with notebooks. When the session expires, any notebooks in the workspace will be removed.";
|
||||
public static newNotebookModalContent2 =
|
||||
"To save your work permanently, save your notebooks to a GitHub repository or download the notebooks to your local machine before the session ends. ";
|
||||
public static galleryNotebookDownloadContent1 =
|
||||
"To download, run, and make changes to this sample notebook, a temporary workspace will be created. When the session expires, any notebooks in the workspace will be removed.";
|
||||
public static galleryNotebookDownloadContent2 =
|
||||
"To save your work permanently, save your notebooks to a GitHub repository or download the Notebooks to your local machine before the session ends. ";
|
||||
public static cosmosNotebookHomePageUrl = "https://aka.ms/cosmos-notebooks-limits";
|
||||
public static cosmosNotebookGitDocumentationUrl = "https://aka.ms/cosmos-notebooks-github";
|
||||
public static learnMore = "Learn more.";
|
||||
}
|
||||
|
||||
export class SparkLibrary {
|
||||
|
||||
@@ -3,8 +3,16 @@ import { resetConfigContext, updateConfigContext } from "../ConfigContext";
|
||||
import { DatabaseAccount } from "../Contracts/DataModels";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
import {
|
||||
deleteDocument,
|
||||
getEndpoint,
|
||||
getFeatureEndpointOrDefault,
|
||||
queryDocuments,
|
||||
readDocument,
|
||||
updateDocument,
|
||||
} from "./MongoProxyClient";
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
@@ -246,4 +254,31 @@ describe("MongoProxyClient", () => {
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/guest/mongo/explorer");
|
||||
});
|
||||
});
|
||||
describe("getFeatureEndpointOrDefault", () => {
|
||||
beforeEach(() => {
|
||||
resetConfigContext();
|
||||
updateConfigContext({
|
||||
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
"feature.mongoProxyEndpoint": "https://localhost:12901",
|
||||
"feature.mongoProxyAPIs": "readDocument|createDocument",
|
||||
});
|
||||
const features = extractFeatures(params);
|
||||
updateUserContext({
|
||||
authType: AuthType.AAD,
|
||||
features: features,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns a local endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
expect(endpoint).toEqual("https://localhost:12901/api/mongo/explorer");
|
||||
});
|
||||
|
||||
it("returns a production endpoint", () => {
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
expect(endpoint).toEqual("https://main.documentdb.ext.azure.com/api/mongo/explorer");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as DataModels from "../Contracts/DataModels";
|
||||
import { MessageTypes } from "../Contracts/ExplorerContracts";
|
||||
import { Collection } from "../Contracts/ViewModels";
|
||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { hasFlag } from "../Platform/Hosted/extractFeatures";
|
||||
import { userContext } from "../UserContext";
|
||||
import { logConsoleError } from "../Utils/NotificationConsoleUtils";
|
||||
import { ApiType, HttpHeaders, HttpStatusCodes } from "./Constants";
|
||||
@@ -78,7 +79,7 @@ export function queryDocuments(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint() || "";
|
||||
const endpoint = getFeatureEndpointOrDefault("resourcelist") || "";
|
||||
|
||||
const headers = {
|
||||
...defaultHeaders,
|
||||
@@ -141,7 +142,8 @@ export function readDocument(
|
||||
: "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("readDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
method: "GET",
|
||||
@@ -181,7 +183,7 @@ export function createDocument(
|
||||
pk: collection && collection.partitionKey && !collection.partitionKey.systemKey ? partitionKeyProperty : "",
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}/resourcelist?${queryString.stringify(params)}`, {
|
||||
@@ -225,7 +227,7 @@ export function updateDocument(
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("updateDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -266,7 +268,7 @@ export function deleteDocument(databaseId: string, collection: Collection, docum
|
||||
? documentId.partitionKeyProperty
|
||||
: "",
|
||||
};
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("deleteDocument");
|
||||
|
||||
return window
|
||||
.fetch(`${endpoint}?${queryString.stringify(params)}`, {
|
||||
@@ -309,7 +311,7 @@ export function createMongoCollectionWithProxy(
|
||||
autoPilotThroughput: params.autoPilotMaxThroughput?.toString(),
|
||||
};
|
||||
|
||||
const endpoint = getEndpoint();
|
||||
const endpoint = getFeatureEndpointOrDefault("createCollectionWithProxy");
|
||||
|
||||
return window
|
||||
.fetch(
|
||||
@@ -333,8 +335,15 @@ export function createMongoCollectionWithProxy(
|
||||
});
|
||||
}
|
||||
|
||||
export function getEndpoint(): string {
|
||||
let url = (configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT) + "/api/mongo/explorer";
|
||||
export function getFeatureEndpointOrDefault(feature: string): string {
|
||||
return hasFlag(userContext.features.mongoProxyAPIs, feature)
|
||||
? getEndpoint(userContext.features.mongoProxyEndpoint)
|
||||
: getEndpoint();
|
||||
}
|
||||
|
||||
export function getEndpoint(customEndpoint?: string): string {
|
||||
let url = customEndpoint ? customEndpoint : configContext.MONGO_BACKEND_ENDPOINT || configContext.BACKEND_ENDPOINT;
|
||||
url += "/api/mongo/explorer";
|
||||
|
||||
if (userContext.authType === AuthType.EncryptedToken) {
|
||||
url = url.replace("api/mongo", "api/guest/mongo");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MutableRefObject, useEffect, useRef } from "react";
|
||||
import arrowLeftImg from "../../images/imgarrowlefticon.svg";
|
||||
import refreshImg from "../../images/refresh-cosmos.svg";
|
||||
import { AuthType } from "../AuthType";
|
||||
@@ -6,6 +6,7 @@ import Explorer from "../Explorer/Explorer";
|
||||
import { ResourceTokenTree } from "../Explorer/Tree/ResourceTokenTree";
|
||||
import { ResourceTree } from "../Explorer/Tree/ResourceTree";
|
||||
import { userContext } from "../UserContext";
|
||||
import { NormalizedEventKey } from "./Constants";
|
||||
|
||||
export interface ResourceTreeContainerProps {
|
||||
toggleLeftPaneExpanded: () => void;
|
||||
@@ -18,6 +19,22 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
isLeftPaneExpanded,
|
||||
container,
|
||||
}: ResourceTreeContainerProps): JSX.Element => {
|
||||
const focusButton = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLeftPaneExpanded) {
|
||||
if (focusButton.current) {
|
||||
focusButton.current.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyPressToggleLeftPaneExpanded = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
toggleLeftPaneExpanded();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div id="main" className={isLeftPaneExpanded ? "main" : "hiddenMain"}>
|
||||
{/* Collections Window - - Start */}
|
||||
@@ -43,9 +60,11 @@ export const ResourceTreeContainer: FunctionComponent<ResourceTreeContainerProps
|
||||
id="expandToggleLeftPaneButton"
|
||||
role="button"
|
||||
onClick={toggleLeftPaneExpanded}
|
||||
onKeyPress={onKeyPressToggleLeftPaneExpanded}
|
||||
tabIndex={0}
|
||||
aria-label="Collapse Tree"
|
||||
title="Collapse Tree"
|
||||
ref={focusButton}
|
||||
>
|
||||
<img className="refreshcol1" src={arrowLeftImg} alt="Hide" />
|
||||
</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ export const InfoTooltip: React.FunctionComponent<TooltipProps> = ({ children }:
|
||||
return (
|
||||
<span>
|
||||
<TooltipHost content={children}>
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" ariaLabel="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Icon, Label, Stack } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import { accordionStackTokens } from "../Settings/SettingsRenderUtils";
|
||||
|
||||
export interface CollapsibleSectionProps {
|
||||
@@ -30,6 +31,13 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
}
|
||||
}
|
||||
|
||||
private onKeyPress = (event: React.KeyboardEvent) => {
|
||||
if (event.key === NormalizedEventKey.Space || event.key === NormalizedEventKey.Enter) {
|
||||
this.toggleCollapsed();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
@@ -39,6 +47,11 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
verticalAlign="center"
|
||||
tokens={accordionStackTokens}
|
||||
onClick={this.toggleCollapsed}
|
||||
onKeyPress={this.onKeyPress}
|
||||
tabIndex={0}
|
||||
aria-name="Advanced"
|
||||
role="button"
|
||||
aria-expanded={this.state.isExpanded}
|
||||
>
|
||||
<Icon iconName={this.state.isExpanded ? "ChevronDown" : "ChevronRight"} />
|
||||
<Label>{this.props.title}</Label>
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
exports[`CollapsibleSectionComponent renders 1`] = `
|
||||
<Fragment>
|
||||
<Stack
|
||||
aria-expanded={true}
|
||||
aria-name="Advanced"
|
||||
className="collapsibleSection"
|
||||
horizontal={true}
|
||||
onClick={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 10,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Link,
|
||||
PrimaryButton,
|
||||
ProgressIndicator,
|
||||
Text,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import React, { FC } from "react";
|
||||
@@ -30,6 +31,7 @@ export interface DialogState {
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
contentHtml?: JSX.Element,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
@@ -58,6 +60,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void,
|
||||
contentHtml?: JSX.Element,
|
||||
choiceGroupProps?: IChoiceGroupProps,
|
||||
textFieldProps?: TextFieldProps,
|
||||
primaryButtonDisabled?: boolean
|
||||
@@ -76,6 +79,7 @@ export const useDialog: UseStore<DialogState> = create((set, get) => ({
|
||||
get().closeDialog();
|
||||
onCancel && onCancel();
|
||||
},
|
||||
contentHtml,
|
||||
choiceGroupProps,
|
||||
textFieldProps,
|
||||
primaryButtonDisabled,
|
||||
@@ -124,6 +128,7 @@ export interface DialogProps {
|
||||
type?: DialogType;
|
||||
showCloseButton?: boolean;
|
||||
onDismiss?: () => void;
|
||||
contentHtml?: JSX.Element;
|
||||
}
|
||||
|
||||
const DIALOG_MIN_WIDTH = "400px";
|
||||
@@ -150,6 +155,7 @@ export const Dialog: FC = () => {
|
||||
type,
|
||||
showCloseButton,
|
||||
onDismiss,
|
||||
contentHtml,
|
||||
} = props || {};
|
||||
|
||||
const dialogProps: IDialogProps = {
|
||||
@@ -191,6 +197,7 @@ export const Dialog: FC = () => {
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
{contentHtml && <Text>{contentHtml}</Text>}
|
||||
{progressIndicatorProps && <ProgressIndicator {...progressIndicatorProps} />}
|
||||
<DialogFooter>
|
||||
<PrimaryButton {...primaryButtonProps} />
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
.input-type-head-text-field {
|
||||
width: 100%;
|
||||
}
|
||||
.input-query-form {
|
||||
width: 100%;
|
||||
}
|
||||
textarea {
|
||||
width: 100%;
|
||||
line-height: 1;
|
||||
|
||||
@@ -160,18 +160,21 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
return (
|
||||
<div className="input-typeahead-container">
|
||||
<Stack horizontal>
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
<form aria-labelledby="input" className="input-query-form">
|
||||
<TextField
|
||||
multiline={useTextarea}
|
||||
rows={1}
|
||||
id="input"
|
||||
defaultValue={defaultValue}
|
||||
ariaLabel="Input query"
|
||||
placeholder={placeholder}
|
||||
className="input-type-head-text-field"
|
||||
value={defaultValue}
|
||||
onKeyDown={this.onSubmit}
|
||||
onFocus={() => this.setState({ isSuggestionVisible: true })}
|
||||
onChange={(_event, newValue?: string) => this.handleChange(newValue)}
|
||||
/>
|
||||
</form>
|
||||
{this.props.showCancelButton && (
|
||||
<IconButton
|
||||
styles={iconButtonStyles}
|
||||
|
||||
@@ -7,16 +7,22 @@ exports[`inputTypeahead renders <input /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={false}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</form>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
@@ -28,16 +34,22 @@ exports[`inputTypeahead renders <textarea /> 1`] = `
|
||||
<Stack
|
||||
horizontal={true}
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
<form
|
||||
aria-labelledby="input"
|
||||
className="input-query-form"
|
||||
>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Input query"
|
||||
className="input-type-head-text-field"
|
||||
id="input"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
onFocus={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
placeholder="placeholder"
|
||||
rows={1}
|
||||
/>
|
||||
</form>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -17,6 +17,8 @@ import Explorer from "../../Explorer";
|
||||
import { NotebookClientV2 } from "../../Notebook/NotebookClientV2";
|
||||
import { NotebookComponentBootstrapper } from "../../Notebook/NotebookComponent/NotebookComponentBootstrapper";
|
||||
import NotebookReadOnlyRenderer from "../../Notebook/NotebookRenderer/NotebookReadOnlyRenderer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import { Dialog, TextFieldProps, useDialog } from "../Dialog";
|
||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||
import "./NotebookViewerComponent.less";
|
||||
@@ -146,7 +148,9 @@ export class NotebookViewerComponent
|
||||
<NotebookMetadataComponent
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={this.props.container && "Download to my notebooks"}
|
||||
downloadButtonText={
|
||||
this.props.container && NotebookUtil.getNotebookBtnTitle(useNotebook.getState().notebookFolderName)
|
||||
}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
import * as React from "react";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import { Urls, StyleConstants } from "../../../Common/Constants";
|
||||
import {
|
||||
getPriceCurrency,
|
||||
getCurrencySign,
|
||||
getAutoscalePricePerRu,
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
estimatedCostDisclaimer,
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
DetailsRow,
|
||||
ICheckboxStyles,
|
||||
IStackProps,
|
||||
IStackTokens,
|
||||
IChoiceGroupStyles,
|
||||
Link,
|
||||
Text,
|
||||
IMessageBarStyles,
|
||||
ITextStyles,
|
||||
IDetailsRowStyles,
|
||||
IStackStyles,
|
||||
IColumn,
|
||||
IDetailsColumnStyles,
|
||||
IDetailsListStyles,
|
||||
IDetailsRowProps,
|
||||
IDetailsRowStyles,
|
||||
IDropdownStyles,
|
||||
IMessageBarStyles,
|
||||
ISeparatorStyles,
|
||||
IStackProps,
|
||||
IStackStyles,
|
||||
IStackTokens,
|
||||
ITextFieldStyles,
|
||||
ITextStyles,
|
||||
Link,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
Stack,
|
||||
SelectionMode,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
DetailsList,
|
||||
IColumn,
|
||||
SelectionMode,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles,
|
||||
Stack,
|
||||
Text,
|
||||
} from "@fluentui/react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
import * as React from "react";
|
||||
import { StyleConstants, Urls } from "../../../Common/Constants";
|
||||
import { AutopilotDocumentation, hoursInAMonth } from "../../../Shared/Constants";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import {
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
getAutoscalePricePerRu,
|
||||
getCurrencySign,
|
||||
getMultimasterMultiplier,
|
||||
getPriceCurrency,
|
||||
getPricePerRu,
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import { isDirty, isDirtyTypes } from "./SettingsUtils";
|
||||
|
||||
export interface EstimatedSpendingDisplayProps {
|
||||
costType: JSX.Element;
|
||||
@@ -65,7 +65,7 @@ export interface PriceBreakdown {
|
||||
currencySign: string;
|
||||
}
|
||||
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14, color: "windowtext" } };
|
||||
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
@@ -223,14 +223,15 @@ export const getRuPriceBreakdown = (
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
: getPricePerRu(serverId);
|
||||
const multimasterMultiplier = getMultimasterMultiplier(numberOfRegions, isMultimaster);
|
||||
const pricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multimasterMultiplier)
|
||||
: getPricePerRu(serverId, multimasterMultiplier);
|
||||
return {
|
||||
hourlyPrice: hourlyPrice,
|
||||
hourlyPrice,
|
||||
dailyPrice: hourlyPrice * 24,
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
pricePerRu,
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId),
|
||||
};
|
||||
@@ -271,7 +272,7 @@ export const manualToAutoscaleDisclaimerElement: JSX.Element = (
|
||||
<Text styles={infoAndToolTipTextStyle} id="manualToAutoscaleDisclaimerElement">
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings
|
||||
and storage of your resource. After autoscale has been enabled, you can change the max RU/s.{" "}
|
||||
<a href={Urls.autoscaleMigration}>Learn more</a>
|
||||
<Link href={Urls.autoscaleMigration}>Learn more</Link>
|
||||
</Text>
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ exports[`IndexingPolicyRefreshComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -39,6 +40,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -73,6 +75,7 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -80,11 +83,11 @@ exports[`ThroughputInputAutoPilotV3Component autopilot input visible 1`] = `
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
</StyledMessageBar>
|
||||
<StyledChoiceGroup
|
||||
@@ -186,6 +189,7 @@ exports[`ThroughputInputAutoPilotV3Component spendAck checkbox visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -460,6 +464,7 @@ exports[`ThroughputInputAutoPilotV3Component throughput input visible 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ exports[`ScaleComponent renders with correct initial notification 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ exports[`SubSettingsComponent analyticalTimeToLive hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -412,6 +413,7 @@ exports[`SubSettingsComponent analyticalTimeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -952,6 +954,7 @@ exports[`SubSettingsComponent renders 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -1228,6 +1231,7 @@ exports[`SubSettingsComponent timeToLiveSeconds hidden 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -166,16 +167,17 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
>
|
||||
The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s.
|
||||
|
||||
<a
|
||||
<StyledLinkBase
|
||||
href="https://aka.ms/cosmos-autoscale-migration"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</StyledLinkBase>
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -195,6 +197,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -207,6 +210,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -219,6 +223,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -230,6 +235,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -249,6 +255,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -264,6 +271,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -278,6 +286,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -291,6 +300,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -302,6 +312,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -321,6 +332,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -363,6 +375,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -378,6 +391,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
@@ -394,6 +408,7 @@ exports[`SettingsUtils functions render 1`] = `
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "windowtext",
|
||||
"fontSize": 14,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@import "../../../../less/Common/Constants";
|
||||
|
||||
.tabComponentContainer {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
.flex-display();
|
||||
.flex-direction();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { userContext } from "../../../../UserContext";
|
||||
import {
|
||||
calculateEstimateNumber,
|
||||
computeRUUsagePriceHourly,
|
||||
estimatedCostDisclaimer,
|
||||
getAutoscalePricePerRu,
|
||||
getCurrencySign,
|
||||
getMultimasterMultiplier,
|
||||
@@ -42,11 +43,9 @@ export const CostEstimateText: FunctionComponent<CostEstimateTextProps> = ({
|
||||
const currency: string = getPriceCurrency(serverId);
|
||||
const currencySign: string = getCurrencySign(serverId);
|
||||
const multiplier = getMultimasterMultiplier(numberOfRegions, multimasterEnabled);
|
||||
const pricePerRu = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, multiplier) * multiplier
|
||||
: getPricePerRu(serverId) * multiplier;
|
||||
const pricePerRu = isAutoscale ? getAutoscalePricePerRu(serverId, multiplier) : getPricePerRu(serverId, multiplier);
|
||||
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>PricingUtils.estimatedCostDisclaimer</InfoTooltip>;
|
||||
const iconWithEstimatedCostDisclaimer: JSX.Element = <InfoTooltip>{estimatedCostDisclaimer}</InfoTooltip>;
|
||||
|
||||
if (isAutoscale) {
|
||||
return (
|
||||
|
||||
@@ -118,6 +118,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<input
|
||||
className="throughputInputRadioBtn"
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={isAutoscaleSelected}
|
||||
type="radio"
|
||||
role="radio"
|
||||
@@ -131,6 +132,7 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
aria-label="Manual mode"
|
||||
checked={!isAutoscaleSelected}
|
||||
type="radio"
|
||||
aria-required={true}
|
||||
role="radio"
|
||||
tabIndex={0}
|
||||
onChange={(e) => handleOnChangeMode(e, "Manual")}
|
||||
|
||||
@@ -345,12 +345,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -630,6 +632,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
@@ -651,6 +654,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-label="Autoscale mode"
|
||||
aria-required={true}
|
||||
checked={true}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.0"
|
||||
@@ -667,6 +671,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
</span>
|
||||
<input
|
||||
aria-label="Manual mode"
|
||||
aria-required={true}
|
||||
checked={false}
|
||||
className="throughputInputRadioBtn"
|
||||
key=".0:$.2"
|
||||
@@ -1327,12 +1332,14 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
tabIndex={0}
|
||||
>
|
||||
<IconBase
|
||||
ariaLabel="Info"
|
||||
className="panelInfoIcon"
|
||||
iconName="Info"
|
||||
styles={[Function]}
|
||||
tabIndex={0}
|
||||
theme={
|
||||
Object {
|
||||
"disableGlobalClassNames": false,
|
||||
@@ -1612,6 +1619,7 @@ exports[`ThroughputInput Pane should render Default properly 1`] = `
|
||||
className="panelInfoIcon root-57"
|
||||
data-icon-name="Info"
|
||||
role="img"
|
||||
tabIndex={0}
|
||||
>
|
||||
|
||||
</i>
|
||||
|
||||
@@ -243,6 +243,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||
<IconButton
|
||||
name="More"
|
||||
title="More"
|
||||
className="treeMenuEllipsis"
|
||||
ariaLabel={menuItemLabel}
|
||||
menuIconProps={{
|
||||
|
||||
@@ -211,6 +211,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,6 +424,7 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
||||
},
|
||||
}
|
||||
}
|
||||
title="More"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Link } from "@fluentui/react/lib/Link";
|
||||
import * as ko from "knockout";
|
||||
import React from "react";
|
||||
import _ from "underscore";
|
||||
import { AuthType } from "../AuthType";
|
||||
import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer";
|
||||
import * as Constants from "../Common/Constants";
|
||||
import { ConnectionStatusType } from "../Common/Constants";
|
||||
import { ConnectionStatusType, HttpStatusCodes, Notebook } from "../Common/Constants";
|
||||
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||
import { isPublicInternetAccessAllowed } from "../Common/DatabaseAccountUtility";
|
||||
@@ -12,6 +13,7 @@ import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHand
|
||||
import * as Logger from "../Common/Logger";
|
||||
import { QueriesClient } from "../Common/QueriesClient";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../Contracts/DataModels";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
@@ -164,23 +166,10 @@ export default class Explorer {
|
||||
|
||||
useNotebook.subscribe(
|
||||
async () => {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = await (
|
||||
await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager")
|
||||
).default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
this.initiateAndRefreshNotebookList();
|
||||
useNotebook.getState().setIsRefreshed(false);
|
||||
},
|
||||
(state) => state.isNotebookEnabled
|
||||
(state) => state.isNotebookEnabled || state.isRefreshed
|
||||
);
|
||||
|
||||
this.resourceTree = new ResourceTreeAdapter(this);
|
||||
@@ -213,6 +202,23 @@ export default class Explorer {
|
||||
this.refreshExplorer();
|
||||
}
|
||||
|
||||
public async initiateAndRefreshNotebookList(): Promise<void> {
|
||||
if (!this.notebookManager) {
|
||||
const NotebookManager = (await import(/* webpackChunkName: "NotebookManager" */ "./Notebook/NotebookManager"))
|
||||
.default;
|
||||
this.notebookManager = new NotebookManager();
|
||||
this.notebookManager.initialize({
|
||||
container: this,
|
||||
resourceTree: this.resourceTree,
|
||||
refreshCommandBarButtons: () => this.refreshCommandBarButtons(),
|
||||
refreshNotebookList: () => this.refreshNotebookList(),
|
||||
});
|
||||
}
|
||||
|
||||
this.refreshCommandBarButtons();
|
||||
this.refreshNotebookList();
|
||||
}
|
||||
|
||||
public openEnableSynapseLinkDialog(): void {
|
||||
const addSynapseLinkDialogProps: DialogProps = {
|
||||
linkProps: {
|
||||
@@ -346,30 +352,7 @@ export default class Explorer {
|
||||
return;
|
||||
}
|
||||
this._isInitializingNotebooks = true;
|
||||
if (userContext.features.phoenix) {
|
||||
const connectionStatus: DataModels.ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Allocating,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
const provisionData = {
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
resourceId: userContext.databaseAccount.id,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
aadToken: userContext.authorizationToken,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
};
|
||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||
if (connectionInfo.data && connectionInfo.data.notebookServerUrl) {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (userContext.features.phoenix === false) {
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
@@ -384,13 +367,59 @@ export default class Explorer {
|
||||
});
|
||||
}
|
||||
|
||||
useNotebook.getState().initializeNotebooksTree(this.notebookManager);
|
||||
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
||||
public async allocateContainer(): Promise<void> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
const isAllocating = useNotebook.getState().isAllocating;
|
||||
if (isAllocating === false && notebookServerInfo && notebookServerInfo.notebookServerEndpoint === undefined) {
|
||||
const provisionData = {
|
||||
aadToken: userContext.authorizationToken,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
dbAccountName: userContext.databaseAccount.name,
|
||||
cosmosEndpoint: userContext.databaseAccount.properties.documentEndpoint,
|
||||
};
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Connecting,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
try {
|
||||
useNotebook.getState().setIsAllocating(true);
|
||||
const connectionInfo = await this.phoenixClient.containerConnectionInfo(provisionData);
|
||||
if (
|
||||
connectionInfo.status === HttpStatusCodes.OK &&
|
||||
connectionInfo.data &&
|
||||
connectionInfo.data.notebookServerUrl
|
||||
) {
|
||||
connectionStatus.status = ConnectionStatusType.Connected;
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.data.notebookServerUrl,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.data.notebookAuthToken,
|
||||
});
|
||||
this.notebookManager?.notebookClient
|
||||
.getMemoryUsage()
|
||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
} else {
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
}
|
||||
} catch (error) {
|
||||
connectionStatus.status = ConnectionStatusType.Failed;
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
throw error;
|
||||
}
|
||||
this.refreshNotebookList();
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
}
|
||||
|
||||
public resetNotebookWorkspace(): void {
|
||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||
handleError(
|
||||
@@ -662,6 +691,9 @@ export default class Explorer {
|
||||
if (!notebookContentItem || !notebookContentItem.path) {
|
||||
throw new Error(`Invalid notebookContentItem: ${notebookContentItem}`);
|
||||
}
|
||||
if (notebookContentItem.type === NotebookContentItemType.Notebook && NotebookUtil.isPhoenixEnabled()) {
|
||||
this.allocateContainer();
|
||||
}
|
||||
|
||||
const notebookTabs = useTabs
|
||||
.getState()
|
||||
@@ -883,9 +915,51 @@ export default class Explorer {
|
||||
handleError(error, "Explorer/onNewNotebookClicked");
|
||||
throw new Error(error);
|
||||
}
|
||||
const isPhoenixEnabled = NotebookUtil.isPhoenixEnabled();
|
||||
if (isPhoenixEnabled) {
|
||||
if (isGithubTree) {
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
};
|
||||
} else {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookModalTitle,
|
||||
undefined,
|
||||
"Create",
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.createNewNoteBook(parent, isGithubTree);
|
||||
}
|
||||
}
|
||||
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
private getNewNoteWarningText(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.newNotebookModalContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.newNotebookModalContent2}
|
||||
<Link href={Notebook.cosmosNotebookHomePageUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private createNewNoteBook(parent?: NotebookContentItem, isGithubTree?: boolean): void {
|
||||
const clearInProgressMessage = logConsoleProgress(`Creating new notebook in ${parent.path}`);
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateNewNotebook, {
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
@@ -932,7 +1006,26 @@ export default class Explorer {
|
||||
await this.notebookManager?.notebookContentClient.updateItemChildrenInPlace(item);
|
||||
}
|
||||
|
||||
public openNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
public async openNotebookTerminal(kind: ViewModels.TerminalKind): Promise<void> {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await this.allocateContainer();
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Failed to Connect",
|
||||
"Failed to connect temporary workspace, this could happen because of network issue please refresh and try again."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.connectToNotebookTerminal(kind);
|
||||
}
|
||||
}
|
||||
|
||||
private connectToNotebookTerminal(kind: ViewModels.TerminalKind): void {
|
||||
let title: string;
|
||||
|
||||
switch (kind) {
|
||||
@@ -983,7 +1076,7 @@ export default class Explorer {
|
||||
notebookUrl?: string,
|
||||
galleryItem?: IGalleryItem,
|
||||
isFavorite?: boolean
|
||||
) {
|
||||
): Promise<void> {
|
||||
const title = "Gallery";
|
||||
const GalleryTab = await (await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab")).default;
|
||||
const galleryTab = useTabs
|
||||
@@ -1087,7 +1180,27 @@ export default class Explorer {
|
||||
}
|
||||
|
||||
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
Notebook.newNotebookUploadModalTitle,
|
||||
undefined,
|
||||
"Upload",
|
||||
async () => {
|
||||
await this.allocateContainer();
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
this.getNewNoteWarningText()
|
||||
);
|
||||
} else {
|
||||
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||
this.uploadFilePanel(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadFilePanel(parent?: NotebookContentItem): void {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -1096,6 +1209,24 @@ export default class Explorer {
|
||||
);
|
||||
}
|
||||
|
||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
return (
|
||||
<>
|
||||
<p>{Notebook.galleryNotebookDownloadContent1}</p>
|
||||
<br />
|
||||
<p>
|
||||
{Notebook.galleryNotebookDownloadContent2}
|
||||
<Link href={Notebook.cosmosNotebookGitDocumentationUrl} target="_blank">
|
||||
{Notebook.learnMore}
|
||||
</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <p> Download {fileName} from gallery as a copy to your notebooks to run and/or edit the notebook. </p>;
|
||||
}
|
||||
|
||||
public async refreshExplorer(): Promise<void> {
|
||||
userContext.authType === AuthType.ResourceToken
|
||||
? this.refreshDatabaseForResourceToken()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FeedOptions, ItemDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||
import * as Q from "q";
|
||||
import * as React from "react";
|
||||
@@ -294,8 +296,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.setGremlinParams();
|
||||
}
|
||||
|
||||
const selectedNode = this.state.highlightedNode;
|
||||
|
||||
props.onGraphAccessorCreated({
|
||||
applyFilter: this.submitQuery.bind(this),
|
||||
addVertex: this.addVertex.bind(this),
|
||||
@@ -303,7 +303,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
});
|
||||
} // constructor
|
||||
|
||||
public shareIGraphConfig(igraphConfig: IGraphConfig) {
|
||||
public shareIGraphConfig(igraphConfig: IGraphConfig): void {
|
||||
this.setState({
|
||||
igraphConfig: { ...igraphConfig },
|
||||
});
|
||||
@@ -330,10 +330,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const partitionKeyProperty = this.props.collectionPartitionKeyProperty;
|
||||
|
||||
// aggregate all the properties, remove dropped ones
|
||||
let finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
||||
const finalProperties = editedProperties.existingProperties.concat(editedProperties.addedProperties);
|
||||
|
||||
// Compose the query
|
||||
let pkId = editedProperties.pkId;
|
||||
const pkId = editedProperties.pkId;
|
||||
let updateQueryFragment = "";
|
||||
|
||||
finalProperties.forEach((p) => {
|
||||
@@ -422,7 +422,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Called from ko binding
|
||||
* @param id
|
||||
*/
|
||||
public selectNode(id: string) {
|
||||
public selectNode(id: string): void {
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to select node, but d3ForceGraph not initialized, yet.");
|
||||
return;
|
||||
@@ -431,7 +431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.d3ForceGraph.selectNode(id);
|
||||
}
|
||||
|
||||
public deleteHighlightedNode() {
|
||||
public deleteHighlightedNode(): void {
|
||||
if (!this.state.highlightedNode) {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, "No highlighted node to remove.");
|
||||
return;
|
||||
@@ -467,23 +467,23 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Is of type: {e: GremlinEdge, v: GremlinVertex}[]
|
||||
* @param data
|
||||
*/
|
||||
public static isEdgeVertexPairArray(data: any) {
|
||||
public static isEdgeVertexPairArray(data: any): boolean {
|
||||
if (!(data instanceof Array)) {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, "Query result not an array", data);
|
||||
return false;
|
||||
}
|
||||
|
||||
let pairs: any[] = data;
|
||||
const pairs: any[] = data;
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const item = pairs[i];
|
||||
if (
|
||||
!item.hasOwnProperty("e") ||
|
||||
!item.hasOwnProperty("v") ||
|
||||
!item["e"].hasOwnProperty("id") ||
|
||||
!item["e"].hasOwnProperty("type") ||
|
||||
!Object.prototype.hasOwnProperty.call(item, "e") ||
|
||||
!Object.prototype.hasOwnProperty.call(item, "v") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||
item["e"].type !== "edge" ||
|
||||
!item["v"].hasOwnProperty("id") ||
|
||||
!item["v"].hasOwnProperty("type") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["v"], "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(item["e"], "type") ||
|
||||
item["v"].type !== "vertex"
|
||||
) {
|
||||
return false;
|
||||
@@ -514,7 +514,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// Try hitting cache first
|
||||
const cache = outE ? this.outECache : this.inECache;
|
||||
const pairs = cache.retrieve(vertex.id, startIndex, pageSize);
|
||||
if (pairs != null && pairs.length === pageSize) {
|
||||
if (pairs !== null && pairs.length === pageSize) {
|
||||
const msg = `Retrieved ${pairs.length} ${outE ? "outE" : "inE"} edges from cache for vertex id: ${vertex.id}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Info, msg);
|
||||
return Q.resolve(pairs);
|
||||
@@ -588,7 +588,6 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
vertex._outEAllLoaded &&
|
||||
vertex._inEAllLoaded
|
||||
) {
|
||||
console.info("No more edges to load for vertex " + vertex.id);
|
||||
updateGraphData();
|
||||
return Q.resolve(graphData);
|
||||
}
|
||||
@@ -668,7 +667,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}
|
||||
);
|
||||
|
||||
return promise.then((nbPairsFetched: number) => {
|
||||
return promise.then(() => {
|
||||
if (offsetIndex >= GraphExplorer.LOAD_PAGE_SIZE || !vertex._outEAllLoaded || !vertex._inEAllLoaded) {
|
||||
vertex._pagination = {
|
||||
total:
|
||||
@@ -754,7 +753,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Create a new edge in docdb and update graph
|
||||
* @param e
|
||||
*/
|
||||
public createNewEdge(e: GraphNewEdgeData): Q.Promise<any> {
|
||||
public createNewEdge(e: GraphNewEdgeData): Q.Promise<unknown> {
|
||||
const q = `g.V('${GraphUtil.escapeSingleQuotes(e.inputOutV)}').addE('${GraphUtil.escapeSingleQuotes(
|
||||
e.label
|
||||
)}').To(g.V('${GraphUtil.escapeSingleQuotes(e.inputInV)}'))`;
|
||||
@@ -772,8 +771,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
let edge = edges[0];
|
||||
let graphData = this.originalGraphData;
|
||||
const edge = edges[0];
|
||||
const graphData = this.originalGraphData;
|
||||
graphData.addEdge(edge);
|
||||
|
||||
// Allow loadNeighbors to load list new edge
|
||||
@@ -800,10 +799,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Manually update in-memory graph.
|
||||
* @param edgeId
|
||||
*/
|
||||
public removeEdge(edgeId: string): Q.Promise<any> {
|
||||
public removeEdge(edgeId: string): Q.Promise<unknown> {
|
||||
return this.submitToBackend(`g.E('${GraphUtil.escapeSingleQuotes(edgeId)}').drop()`).then(
|
||||
() => {
|
||||
let graphData = this.originalGraphData;
|
||||
const graphData = this.originalGraphData;
|
||||
graphData.removeEdge(edgeId, false);
|
||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||
},
|
||||
@@ -826,10 +825,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return false;
|
||||
}
|
||||
|
||||
let vertices: any[] = data;
|
||||
const vertices: any[] = data;
|
||||
if (vertices.length > 0) {
|
||||
let v0 = vertices[0];
|
||||
if (!v0.hasOwnProperty("id") || !v0.hasOwnProperty("type") || v0.type !== "vertex") {
|
||||
const v0 = vertices[0];
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(v0, "id") ||
|
||||
!Object.prototype.hasOwnProperty.call(v0, "type") ||
|
||||
v0.type !== "vertex"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -837,7 +840,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}
|
||||
|
||||
public processGremlinQueryResults(result: GremlinClient.GremlinRequestResult): void {
|
||||
const data = result.data as any;
|
||||
const data = result.data as GraphData.GremlinVertex[];
|
||||
this.setFilterQueryStatus(FilterQueryStatus.GraphEmptyResult);
|
||||
|
||||
if (data === null) {
|
||||
@@ -927,13 +930,13 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
throw { title: err };
|
||||
}
|
||||
|
||||
if (vertices == null || vertices.length < 1) {
|
||||
if (vertices === null || vertices.length < 1) {
|
||||
const err = "Failed to create vertex (no vertex in response)";
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, err, vertices);
|
||||
throw { title: err };
|
||||
}
|
||||
|
||||
let vertex = vertices[0];
|
||||
const vertex = vertices[0];
|
||||
const graphData = this.originalGraphData;
|
||||
graphData.addVertex(vertex);
|
||||
this.updateGraphData(graphData, this.state.igraphConfig);
|
||||
@@ -1022,7 +1025,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.gremlinClient.destroy();
|
||||
}
|
||||
public componentDidMount(): void {
|
||||
if (this.props.onLoadStartKey != null && this.props.onLoadStartKey != undefined) {
|
||||
if (this.props.onLoadStartKey !== null && this.props.onLoadStartKey !== undefined) {
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.Tab,
|
||||
{
|
||||
@@ -1082,7 +1085,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
public static reportToConsole(type: ConsoleDataType.Info, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType.Error, msg: string, ...errorData: any[]): void;
|
||||
public static reportToConsole(type: ConsoleDataType, msg: string, ...errorData: any[]): void | (() => void) {
|
||||
let errorDataStr: string = "";
|
||||
let errorDataStr = "";
|
||||
if (errorData && errorData.length > 0) {
|
||||
console.error(msg, errorData);
|
||||
errorDataStr = ": " + JSON.stringify(errorData);
|
||||
@@ -1161,12 +1164,15 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
)}"`
|
||||
).then(
|
||||
(documents: DataModels.DocumentId[]) => {
|
||||
$.each(documents, (index: number, doc: any) => {
|
||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||
data: doc["icon"],
|
||||
format: doc["format"],
|
||||
};
|
||||
});
|
||||
$.each(
|
||||
documents,
|
||||
(index: number, doc: { _graph_icon_property_value: string; icon: string; format: string }) => {
|
||||
newIconsMap[doc["_graph_icon_property_value"]] = {
|
||||
data: doc["icon"],
|
||||
format: doc["format"],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Update graph configuration
|
||||
this.setState({
|
||||
@@ -1223,8 +1229,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
const key = this.state.igraphConfig.nodeCaption;
|
||||
return $.map(
|
||||
this.state.rootMap,
|
||||
(value: any, index: number): LeftPane.CaptionId => {
|
||||
let result = GraphData.GraphData.getNodePropValue(value, key);
|
||||
(value: any): LeftPane.CaptionId => {
|
||||
const result = GraphData.GraphData.getNodePropValue(value, key);
|
||||
return {
|
||||
caption: result !== undefined ? result : value.id,
|
||||
id: value.id,
|
||||
@@ -1237,7 +1243,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* Selecting a root node means
|
||||
* @param node
|
||||
*/
|
||||
private selectRootNode(id: string): Q.Promise<any> {
|
||||
private selectRootNode(id: string): Q.Promise<unknown> {
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to reset zoom, but d3ForceGraph not initialized, yet.");
|
||||
} else {
|
||||
@@ -1282,7 +1288,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
this.collectNodeProperties(this.originalGraphData.vertices);
|
||||
this.updatePropertiesPane(id);
|
||||
},
|
||||
(reason: any) => {
|
||||
(reason: string) => {
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, `Failed to select root node. Reason:${reason}`);
|
||||
}
|
||||
);
|
||||
@@ -1349,10 +1355,10 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private getPkIdFromVertex(v: GraphData.GremlinVertex): string {
|
||||
if (
|
||||
this.props.collectionPartitionKeyProperty &&
|
||||
v.hasOwnProperty("properties") &&
|
||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty) &&
|
||||
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty) &&
|
||||
v.properties[this.props.collectionPartitionKeyProperty].length > 0 &&
|
||||
v.properties[this.props.collectionPartitionKeyProperty][0].hasOwnProperty("value")
|
||||
Object.prototype.hasOwnProperty.call(v.properties[this.props.collectionPartitionKeyProperty][0], "value")
|
||||
) {
|
||||
const pk = v.properties[this.props.collectionPartitionKeyProperty][0].value;
|
||||
return GraphExplorer.generatePkIdPair(pk, v.id);
|
||||
@@ -1370,8 +1376,8 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private getPkIdFromNodeData(v: GraphHighlightedNodeData): string {
|
||||
if (
|
||||
this.props.collectionPartitionKeyProperty &&
|
||||
v.hasOwnProperty("properties") &&
|
||||
v.properties.hasOwnProperty(this.props.collectionPartitionKeyProperty)
|
||||
Object.prototype.hasOwnProperty.call(v, "properties") &&
|
||||
Object.prototype.hasOwnProperty.call(v.properties, this.props.collectionPartitionKeyProperty)
|
||||
) {
|
||||
const pk = v.properties[this.props.collectionPartitionKeyProperty];
|
||||
return GraphExplorer.generatePkIdPair(pk[0] as PartitionKeyValueType, v.id);
|
||||
@@ -1388,14 +1394,14 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @return id
|
||||
*/
|
||||
public static getPkIdFromDocumentId(d: DataModels.DocumentId, collectionPartitionKeyProperty: string): string {
|
||||
let { id } = d;
|
||||
const { id } = d;
|
||||
if (typeof id !== "string") {
|
||||
const error = `Vertex id is not a string: ${JSON.stringify(id)}.`;
|
||||
logConsoleError(error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
||||
if (collectionPartitionKeyProperty && Object.prototype.hasOwnProperty.call(d, collectionPartitionKeyProperty)) {
|
||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||
if (Array.isArray(pk) && pk.length > 0) {
|
||||
@@ -1425,7 +1431,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
}"] AS p FROM c WHERE NOT IS_DEFINED(c._isEdge)`;
|
||||
return this.executeNonPagedDocDbQuery(q).then(
|
||||
(documents: DataModels.DocumentId[]) => {
|
||||
let possibleVertices = [] as PossibleVertex[];
|
||||
const possibleVertices = [] as PossibleVertex[];
|
||||
$.each(documents, (index: number, item: any) => {
|
||||
if (highlightedNodeId && item.id === highlightedNodeId) {
|
||||
// Exclude highlighed node in the list
|
||||
@@ -1439,7 +1445,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
caption: item.p,
|
||||
});
|
||||
} else {
|
||||
if (item.hasOwnProperty("p")) {
|
||||
if (Object.prototype.hasOwnProperty.call(item, "p")) {
|
||||
possibleVertices.push({
|
||||
value: item.id,
|
||||
caption: item.p[0]["_value"],
|
||||
@@ -1462,17 +1468,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
* @param addedEdges
|
||||
* @return promise when done
|
||||
*/
|
||||
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<any> {
|
||||
let promises = [];
|
||||
private editGraphEdges(editedEdges: EditedEdges): Q.Promise<unknown> {
|
||||
const promises = [];
|
||||
// Drop edges
|
||||
for (let i = 0; i < editedEdges.droppedIds.length; i++) {
|
||||
let id = editedEdges.droppedIds[i];
|
||||
const id = editedEdges.droppedIds[i];
|
||||
promises.push(this.removeEdge(id));
|
||||
}
|
||||
|
||||
// Add edges
|
||||
for (let i = 0; i < editedEdges.addedEdges.length; i++) {
|
||||
let e = editedEdges.addedEdges[i];
|
||||
const e = editedEdges.addedEdges[i];
|
||||
promises.push(
|
||||
this.createNewEdge(e).then(() => {
|
||||
// Reload neighbors in case we linked to a vertex that isn't loaded in the graph
|
||||
@@ -1525,7 +1531,9 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
/**
|
||||
* For unit testing purposes
|
||||
*/
|
||||
public onGraphUpdated(timestamp: number): void {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public onGraphUpdated(_timestamp: number): void {}
|
||||
|
||||
/**
|
||||
* Get node properties for styling purposes. Result is the union of all properties of all nodes.
|
||||
@@ -1533,17 +1541,17 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
private collectNodeProperties(vertices: GraphData.GremlinVertex[]) {
|
||||
const props = {} as any; // Hashset
|
||||
$.each(vertices, (index: number, item: GraphData.GremlinVertex) => {
|
||||
for (var p in item) {
|
||||
for (const p in item) {
|
||||
// DocDB: Exclude type because it's always 'vertex'
|
||||
if (p !== "type" && typeof (item as any)[p] === "string") {
|
||||
props[p] = true;
|
||||
}
|
||||
}
|
||||
// Inspect properties
|
||||
if (item.hasOwnProperty("properties")) {
|
||||
if (Object.prototype.hasOwnProperty.call(item, "properties")) {
|
||||
// TODO This is DocDB-graph specific
|
||||
// Assume each property value is [{value:... }]
|
||||
for (var f in item.properties) {
|
||||
for (const f in item.properties) {
|
||||
props[f] = true;
|
||||
}
|
||||
}
|
||||
@@ -1570,21 +1578,21 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
let data = this.originalGraphData.getVertexById(id);
|
||||
const data = this.originalGraphData.getVertexById(id);
|
||||
|
||||
// A bit of translation to make it easier to display
|
||||
let props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
||||
for (let p in data.properties) {
|
||||
const props: { [id: string]: ViewModels.GremlinPropertyValueType[] } = {};
|
||||
for (const p in data.properties) {
|
||||
props[p] = data.properties[p].map((gremlinProperty) => gremlinProperty.value);
|
||||
}
|
||||
|
||||
// update neighbors
|
||||
let sources: NeighborVertexBasicInfo[] = [];
|
||||
let targets: NeighborVertexBasicInfo[] = [];
|
||||
const sources: NeighborVertexBasicInfo[] = [];
|
||||
const targets: NeighborVertexBasicInfo[] = [];
|
||||
this.props.onResetDefaultGraphConfigValues();
|
||||
let nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
||||
const nodeCaption = this.state.igraphConfigUiData.nodeCaptionChoice;
|
||||
this.updateSelectedNodeNeighbors(data.id, nodeCaption, sources, targets);
|
||||
let sData: GraphHighlightedNodeData = {
|
||||
const sData: GraphHighlightedNodeData = {
|
||||
id: data.id,
|
||||
label: data.label,
|
||||
properties: props,
|
||||
@@ -1611,16 +1619,16 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
targets: NeighborVertexBasicInfo[]
|
||||
): void {
|
||||
// update neighbors
|
||||
let gd = this.originalGraphData;
|
||||
let v = gd.getVertexById(id);
|
||||
const gd = this.originalGraphData;
|
||||
const v = gd.getVertexById(id);
|
||||
|
||||
// Clear the array while keeping the references
|
||||
sources.length = 0;
|
||||
targets.length = 0;
|
||||
|
||||
let possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
||||
const possibleEdgeLabels = {} as any; // Collect all edge labels in a hashset
|
||||
|
||||
for (let p in v.inE) {
|
||||
for (const p in v.inE) {
|
||||
possibleEdgeLabels[p] = true;
|
||||
const edges = v.inE[p];
|
||||
$.each(edges, (index: number, edge: GraphData.GremlinShortInEdge) => {
|
||||
@@ -1629,7 +1637,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
||||
return;
|
||||
}
|
||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
sources.push({
|
||||
name: caption,
|
||||
id: neighborId,
|
||||
@@ -1639,7 +1647,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
});
|
||||
}
|
||||
|
||||
for (let p in v.outE) {
|
||||
for (const p in v.outE) {
|
||||
possibleEdgeLabels[p] = true;
|
||||
const edges = v.outE[p];
|
||||
$.each(edges, (index: number, edge: GraphData.GremlinShortOutEdge) => {
|
||||
@@ -1648,7 +1656,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
// If id not known, it must be an edge node whose neighbor hasn't been loaded into the graph, yet
|
||||
return;
|
||||
}
|
||||
let caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
const caption = GraphData.GraphData.getNodePropValue(gd.getVertexById(neighborId), nodeCaption) as string;
|
||||
targets.push({
|
||||
name: caption,
|
||||
id: neighborId,
|
||||
@@ -1660,7 +1668,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
this.setState({
|
||||
possibleEdgeLabels: Object.keys(possibleEdgeLabels).map(
|
||||
(value: string, index: number, array: string[]): InputTypeaheadComponent.Item => {
|
||||
(value: string): InputTypeaheadComponent.Item => {
|
||||
return { caption: value, value: value };
|
||||
}
|
||||
),
|
||||
@@ -1681,20 +1689,20 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
return;
|
||||
}
|
||||
|
||||
let updatedVertex = vertices[0];
|
||||
const updatedVertex = vertices[0];
|
||||
if (this.originalGraphData.hasVertexId(updatedVertex.id)) {
|
||||
let currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
||||
const currentVertex = this.originalGraphData.getVertexById(updatedVertex.id);
|
||||
// Copy updated properties
|
||||
if (currentVertex.hasOwnProperty("properties")) {
|
||||
if (Object.prototype.hasOwnProperty.call(currentVertex, "properties")) {
|
||||
delete currentVertex["properties"];
|
||||
}
|
||||
for (var p in updatedVertex) {
|
||||
for (const p in updatedVertex) {
|
||||
(currentVertex as any)[p] = updatedVertex[p];
|
||||
}
|
||||
}
|
||||
|
||||
// TODO This kind of assumes saveVertexProperty is done from property panes.
|
||||
let hn = this.state.highlightedNode;
|
||||
const hn = this.state.highlightedNode;
|
||||
if (hn && hn.id === updatedVertex.id) {
|
||||
this.updatePropertiesPane(hn.id);
|
||||
}
|
||||
@@ -1708,7 +1716,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
igraphConfig?: IGraphConfig
|
||||
) {
|
||||
this.originalGraphData = graphData;
|
||||
let gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
||||
const gd = JSON.parse(JSON.stringify(this.originalGraphData));
|
||||
if (!this.d3ForceGraph) {
|
||||
console.warn("Attempting to update graph, but d3ForceGraph not initialized, yet.");
|
||||
return;
|
||||
@@ -1873,7 +1881,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
promise
|
||||
.then((result: GremlinClient.GremlinRequestResult) => this.processGremlinQueryResults(result))
|
||||
.catch((error: any) => {
|
||||
.catch((error: Error) => {
|
||||
const errorMsg = `Failed to process query result: ${getErrorMessage(error)}`;
|
||||
GraphExplorer.reportToConsole(ConsoleDataType.Error, errorMsg);
|
||||
this.setState({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
import CollapseArrowIcon from "../../../../images/Collapse_arrow_14x14.svg";
|
||||
import ExpandIcon from "../../../../images/Expand_14x14.svg";
|
||||
import LoadingIndicatorIcon from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||
import { GraphVizComponent, GraphVizComponentProps } from "./GraphVizComponent";
|
||||
|
||||
interface MiddlePaneComponentProps {
|
||||
isTabsContentExpanded: boolean;
|
||||
@@ -17,7 +17,14 @@ export class MiddlePaneComponent extends React.Component<MiddlePaneComponentProp
|
||||
<div className="middlePane">
|
||||
<div className="graphTitle">
|
||||
<span className="paneTitle">Graph</span>
|
||||
<span className="graphExpandCollapseBtn pull-right" onClick={this.props.toggleExpandGraph}>
|
||||
<span
|
||||
className="graphExpandCollapseBtn pull-right"
|
||||
onClick={this.props.toggleExpandGraph}
|
||||
role="button"
|
||||
aria-expanded={this.props.isTabsContentExpanded}
|
||||
aria-name="View graph in full screen"
|
||||
tabIndex={0}
|
||||
>
|
||||
<img
|
||||
src={this.props.isTabsContentExpanded ? CollapseArrowIcon : ExpandIcon}
|
||||
alt={this.props.isTabsContentExpanded ? "collapse graph content" : "expand graph content"}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,10 @@ import create, { UseStore } from "zustand";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { useTabs } from "../../../hooks/useTabs";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookUtil } from "../../Notebook/NotebookUtil";
|
||||
import { useSelectedNode } from "../../useSelectedNode";
|
||||
import * as CommandBarComponentButtonFactory from "./CommandBarComponentButtonFactory";
|
||||
import * as CommandBarUtil from "./CommandBarUtil";
|
||||
@@ -54,9 +56,15 @@ export const CommandBar: React.FC<Props> = ({ container }: Props) => {
|
||||
const uiFabricControlButtons = CommandBarUtil.convertButton(controlButtons, backgroundColor);
|
||||
uiFabricControlButtons.forEach((btn: ICommandBarItemProps) => (btn.iconOnly = true));
|
||||
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus("connectionStatus"));
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createConnectionStatus(container, "connectionStatus"));
|
||||
}
|
||||
|
||||
if (useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2) {
|
||||
if (
|
||||
userContext.features.phoenix === false &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
useTabs.getState().activeTab?.tabKind === ViewModels.CollectionTabKind.NotebookV2
|
||||
) {
|
||||
uiFabricControlButtons.unshift(CommandBarUtil.createMemoryTracker("memoryTracker"));
|
||||
}
|
||||
|
||||
|
||||
@@ -307,11 +307,18 @@ function createOpenSynapseLinkDialogButton(container: Explorer): CommandButtonCo
|
||||
|
||||
function createNewDatabase(container: Explorer): CommandButtonComponentProps {
|
||||
const label = "New " + getDatabaseName();
|
||||
const newDatabaseButton = document.activeElement as HTMLElement;
|
||||
|
||||
return {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
useSidePanel.getState().openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={container} />),
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"New " + getDatabaseName(),
|
||||
<AddDatabasePanel explorer={container} buttonElement={newDatabaseButton} />
|
||||
),
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: true,
|
||||
@@ -596,7 +603,7 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
return {
|
||||
iconSrc: GitHubIcon,
|
||||
iconAlt: label,
|
||||
onCommandClick: () =>
|
||||
onCommandClick: () => {
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
@@ -606,7 +613,8 @@ function createManageGitHubAccountButton(container: Explorer): CommandButtonComp
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={junoClient}
|
||||
/>
|
||||
),
|
||||
);
|
||||
},
|
||||
commandButtonLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { StyleConstants } from "../../../Common/Constants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||
import Explorer from "../../Explorer";
|
||||
import { ConnectionStatus } from "./ConnectionStatusComponent";
|
||||
import { MemoryTracker } from "./MemoryTrackerComponent";
|
||||
|
||||
@@ -203,9 +204,9 @@ export const createMemoryTracker = (key: string): ICommandBarItemProps => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createConnectionStatus = (key: string): ICommandBarItemProps => {
|
||||
export const createConnectionStatus = (container: Explorer, key: string): ICommandBarItemProps => {
|
||||
return {
|
||||
key,
|
||||
onRender: () => <ConnectionStatus />,
|
||||
onRender: () => <ConnectionStatus container={container} />,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,77 +3,182 @@
|
||||
.connectionStatusContainer {
|
||||
cursor: default;
|
||||
align-items: center;
|
||||
margin: 0 9px;
|
||||
border: 1px;
|
||||
min-height: 44px;
|
||||
|
||||
> span {
|
||||
padding-right: 12px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-family: @DataExplorerFont;
|
||||
color: @DefaultFontColor;
|
||||
}
|
||||
&:focus{
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
.connectionStatusFailed{
|
||||
color: #bd1919;
|
||||
.commandReactBtn {
|
||||
&:hover {
|
||||
background-color: rgb(238, 247, 255);
|
||||
color: rgb(32, 31, 30);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus{
|
||||
outline: 1px dashed #605e5c;
|
||||
}
|
||||
}
|
||||
.ring-container {
|
||||
.connectedReactBtn {
|
||||
&:hover {
|
||||
background-color: rgb(238, 247, 255);
|
||||
color: rgb(32, 31, 30);
|
||||
cursor: pointer;
|
||||
}
|
||||
&:focus{
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
.connectIcon{
|
||||
margin: 0px 4px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
color: rgb(0, 120, 212);
|
||||
}
|
||||
.status {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ringringGreen {
|
||||
border: 3px solid green;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
display: block;
|
||||
margin-right: 8px;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
font-size: 9px!important;
|
||||
padding: 0px!important;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.status::before,
|
||||
.status::after {
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
.ringringYellow{
|
||||
border: 3px solid #ffbf00;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
.ringringRed{
|
||||
border: 3px solid #bd1919;
|
||||
border-radius: 30px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
position: absolute;
|
||||
margin: .4285em 0em 0em 0.07477em;
|
||||
animation: pulsate 3s ease-out;
|
||||
animation-iteration-count: infinite;
|
||||
opacity: 0.0
|
||||
}
|
||||
@keyframes pulsate {
|
||||
0% {-webkit-transform: scale(0.1, 0.1); opacity: 0.0;}
|
||||
15% {opacity: 0.8;}
|
||||
25% {opacity: 0.6;}
|
||||
45% {opacity: 0.4;}
|
||||
70% {opacity: 0.3;}
|
||||
100% {-webkit-transform: scale(.7, .7); opacity: 0.1;}
|
||||
}
|
||||
.locationGreenDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: green;
|
||||
}
|
||||
.locationYellowDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: #ffbf00;
|
||||
}
|
||||
.locationRedDot{
|
||||
font-size: 20px;
|
||||
margin-right: 0.07em;
|
||||
color: #bd1919;
|
||||
}
|
||||
content: "";
|
||||
}
|
||||
|
||||
.status::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
background-color: rgba(#fff, 0.1);
|
||||
border-radius: 100%;
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0) scale(0);
|
||||
}
|
||||
|
||||
.connected{
|
||||
background-color: green;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(green, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
.connecting{
|
||||
background-color:#ffbf00;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(#ffbf00, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
.failed{
|
||||
background-color:#bd1919;
|
||||
box-shadow:
|
||||
0 0 0 0em rgba(#bd1919, 0),
|
||||
0em 0.05em 0.1em rgba(#000000, 0.2);
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
}
|
||||
|
||||
.status.connecting.is-animating {
|
||||
animation: status-outer-connecting 3000ms infinite;
|
||||
}
|
||||
.status.failed.is-animating {
|
||||
animation: status-outer-failed 3000ms infinite;
|
||||
}
|
||||
.status.connected.is-animating {
|
||||
animation: status-outer-connected 3000ms infinite;
|
||||
}
|
||||
@keyframes status-outer-connected {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #008000, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.6), 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes status-outer-failed {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #bd1919, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #c52d2d, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #b47b7b, 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@keyframes status-outer-connecting {
|
||||
|
||||
0% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #ffbf00, 0em 0.05em 0.1em rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
20% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em #f0dfad, 0em 0.05em 0.1em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
40% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(198, 243, 198, 0.5), 0em 0.05em 0.1em rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
60% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(213, 241, 213, 0.3), 0em 0.05em 0.1em rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
80% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0.5em rgba(0, 128, 0, 0.1), 0em 0.05em 0.1em rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
85% {
|
||||
transform: translate3d(0, 0, 0) scale(1);
|
||||
box-shadow: 0 0 0 0em rgba(0, 128, 0, 0), 0em 0.05em 0.1em rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,21 @@
|
||||
import { Icon, ProgressIndicator, Spinner, SpinnerSize, Stack, TooltipHost } from "@fluentui/react";
|
||||
import { Icon, ProgressIndicator, Stack, TooltipHost } from "@fluentui/react";
|
||||
import { ActionButton } from "@fluentui/react/lib/Button";
|
||||
import * as React from "react";
|
||||
import { ConnectionStatusType } from "../../../Common/Constants";
|
||||
import "../../../../less/hostedexplorer.less";
|
||||
import { ConnectionStatusType, Notebook } from "../../../Common/Constants";
|
||||
import Explorer from "../../Explorer";
|
||||
import { useNotebook } from "../../Notebook/useNotebook";
|
||||
import "../CommandBar/ConnectionStatusComponent.less";
|
||||
|
||||
export const ConnectionStatus: React.FC = (): JSX.Element => {
|
||||
interface Props {
|
||||
container: Explorer;
|
||||
}
|
||||
export const ConnectionStatus: React.FC<Props> = ({ container }: Props): JSX.Element => {
|
||||
const [second, setSecond] = React.useState("00");
|
||||
const [minute, setMinute] = React.useState("00");
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const [counter, setCounter] = React.useState(0);
|
||||
const [statusColor, setStatusColor] = React.useState("locationYellowDot");
|
||||
const [statusColorAnimation, setStatusColorAnimation] = React.useState("ringringYellow");
|
||||
const toolTipContent = "Hosted runtime status.";
|
||||
const [statusColor, setStatusColor] = React.useState("");
|
||||
const [toolTipContent, setToolTipContent] = React.useState("Connect to temporary workspace.");
|
||||
React.useEffect(() => {
|
||||
let intervalId: NodeJS.Timeout;
|
||||
|
||||
@@ -39,39 +43,65 @@ export const ConnectionStatus: React.FC = (): JSX.Element => {
|
||||
};
|
||||
|
||||
const connectionInfo = useNotebook((state) => state.connectionInfo);
|
||||
if (!connectionInfo) {
|
||||
const memoryUsageInfo = useNotebook((state) => state.memoryUsageInfo);
|
||||
|
||||
const totalGB = memoryUsageInfo ? memoryUsageInfo.totalKB / Notebook.memoryGuageToGB : 0;
|
||||
const usedGB = totalGB > 0 ? totalGB - memoryUsageInfo.freeKB / Notebook.memoryGuageToGB : 0;
|
||||
|
||||
if (
|
||||
connectionInfo &&
|
||||
(connectionInfo.status === ConnectionStatusType.Connect || connectionInfo.status === ConnectionStatusType.ReConnect)
|
||||
) {
|
||||
return (
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<span>Connecting</span>
|
||||
<Spinner size={SpinnerSize.medium} />
|
||||
</Stack>
|
||||
<ActionButton className="commandReactBtn" onClick={() => container.allocateContainer()}>
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<Icon iconName="ConnectVirtualMachine" className="connectIcon" />
|
||||
<span>{connectionInfo.status}</span>
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
</ActionButton>
|
||||
);
|
||||
}
|
||||
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Allocating && isActive === false) {
|
||||
|
||||
if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connecting && isActive === false) {
|
||||
setIsActive(true);
|
||||
setStatusColor("status connecting is-animating");
|
||||
setToolTipContent("Connecting to temporary workspace.");
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Connected && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("locationGreenDot");
|
||||
setStatusColorAnimation("ringringGreen");
|
||||
setStatusColor("status connected is-animating");
|
||||
setToolTipContent("Connected to temporary workspace.");
|
||||
} else if (connectionInfo && connectionInfo.status === ConnectionStatusType.Failed && isActive === true) {
|
||||
stopTimer();
|
||||
setStatusColor("locationRedDot");
|
||||
setStatusColorAnimation("ringringRed");
|
||||
setStatusColor("status failed is-animating");
|
||||
setToolTipContent("Click here to Reconnect to temporary workspace.");
|
||||
}
|
||||
return (
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<div className="ring-container">
|
||||
<div className={statusColorAnimation}></div>
|
||||
<Icon iconName="LocationDot" className={statusColor} />
|
||||
</div>
|
||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||
{connectionInfo.status}
|
||||
</span>
|
||||
{connectionInfo.status === ConnectionStatusType.Allocating && isActive && (
|
||||
<ProgressIndicator description={minute + ":" + second} />
|
||||
)}
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
<ActionButton
|
||||
className={connectionInfo.status === ConnectionStatusType.Failed ? "commandReactBtn" : "connectedReactBtn"}
|
||||
onClick={(e: React.MouseEvent<HTMLSpanElement>) =>
|
||||
connectionInfo.status === ConnectionStatusType.Failed ? container.allocateContainer() : e.preventDefault()
|
||||
}
|
||||
>
|
||||
<TooltipHost content={toolTipContent}>
|
||||
<Stack className="connectionStatusContainer" horizontal>
|
||||
<i className={statusColor}></i>
|
||||
<span className={connectionInfo.status === ConnectionStatusType.Failed ? "connectionStatusFailed" : ""}>
|
||||
{connectionInfo.status}
|
||||
</span>
|
||||
{connectionInfo.status === ConnectionStatusType.Connecting && isActive && (
|
||||
<ProgressIndicator description={minute + ":" + second} />
|
||||
)}
|
||||
{connectionInfo.status === ConnectionStatusType.Connected && !isActive && (
|
||||
<ProgressIndicator
|
||||
className={usedGB / totalGB > 0.8 ? "lowMemory" : ""}
|
||||
description={usedGB.toFixed(1) + " of " + totalGB.toFixed(1) + " GB"}
|
||||
percentComplete={usedGB / totalGB}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</TooltipHost>
|
||||
</ActionButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -129,7 +129,7 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " collapsed" : " expanded")}
|
||||
aria-label={"console button" + (this.props.isConsoleExpanded ? " expanded" : " collapsed")}
|
||||
aria-expanded={!this.props.isConsoleExpanded}
|
||||
>
|
||||
<img
|
||||
@@ -205,7 +205,9 @@ export class NotificationConsoleComponent extends React.Component<
|
||||
{item.type === ConsoleDataType.Error && <img className="errorIcon" src={ErrorRedIcon} alt="error" />}
|
||||
{item.type === ConsoleDataType.InProgress && <img className="loaderIcon" src={LoaderIcon} alt="in progress" />}
|
||||
<span className="date">{item.date}</span>
|
||||
<span className="message">{item.message}</span>
|
||||
<span className="message" role="alert" aria-live="assertive">
|
||||
{item.message}
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ exports[`NotificationConsoleComponent renders the console 1`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button expanded"
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -236,7 +236,7 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
</div>
|
||||
<div
|
||||
aria-expanded={true}
|
||||
aria-label="console button expanded"
|
||||
aria-label="console button collapsed"
|
||||
className="expandCollapseButton"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
@@ -340,7 +340,9 @@ exports[`NotificationConsoleComponent renders the console 2`] = `
|
||||
date
|
||||
</span>
|
||||
<span
|
||||
aria-live="assertive"
|
||||
className="message"
|
||||
role="alert"
|
||||
>
|
||||
message
|
||||
</span>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { CellId, CellType, ImmutableNotebook } from "@nteract/commutable";
|
||||
// Vendor modules
|
||||
import {
|
||||
@@ -30,6 +31,19 @@ export interface NotebookComponentBootstrapperOptions {
|
||||
contentRef: ContentRef;
|
||||
}
|
||||
|
||||
interface IWrapModel {
|
||||
name: string;
|
||||
path: string;
|
||||
last_modified: Date;
|
||||
created: string;
|
||||
content: unknown;
|
||||
format: string;
|
||||
mimetype: unknown;
|
||||
size: number;
|
||||
writeable: boolean;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class NotebookComponentBootstrapper {
|
||||
public contentRef: ContentRef;
|
||||
protected renderExtraComponent: () => JSX.Element;
|
||||
@@ -41,7 +55,7 @@ export class NotebookComponentBootstrapper {
|
||||
this.contentRef = options.contentRef;
|
||||
}
|
||||
|
||||
protected static wrapModelIntoContent(name: string, path: string, content: any) {
|
||||
protected static wrapModelIntoContent(name: string, path: string, content: unknown): IWrapModel {
|
||||
return {
|
||||
name,
|
||||
path,
|
||||
@@ -49,7 +63,7 @@ export class NotebookComponentBootstrapper {
|
||||
created: "",
|
||||
content,
|
||||
format: "json",
|
||||
mimetype: null as any,
|
||||
mimetype: undefined,
|
||||
size: 0,
|
||||
writeable: false,
|
||||
type: "notebook",
|
||||
@@ -85,7 +99,7 @@ export class NotebookComponentBootstrapper {
|
||||
};
|
||||
}
|
||||
|
||||
public setContent(name: string, content: any): void {
|
||||
public setContent(name: string, content: unknown): void {
|
||||
this.getStore().dispatch(
|
||||
actions.fetchContentFulfilled({
|
||||
filepath: undefined,
|
||||
@@ -270,7 +284,6 @@ export class NotebookComponentBootstrapper {
|
||||
public isContentDirty(): boolean {
|
||||
const content = selectors.content(this.getStore().getState(), { contentRef: this.contentRef });
|
||||
if (!content) {
|
||||
console.log("No error");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
* Notebook container related stuff
|
||||
*/
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
import { useNotebook } from "./useNotebook";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
@@ -42,7 +45,7 @@ export class NotebookContainerClient {
|
||||
}, delayMs);
|
||||
}
|
||||
|
||||
private async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
public async getMemoryUsage(): Promise<DataModels.MemoryUsageInfo> {
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||
const error = "No server endpoint detected";
|
||||
@@ -75,6 +78,12 @@ export class NotebookContainerClient {
|
||||
freeKB: memoryUsageInfo.free,
|
||||
};
|
||||
}
|
||||
} else if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.ReConnect,
|
||||
};
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(true);
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
@@ -84,6 +93,13 @@ export class NotebookContainerClient {
|
||||
"Connection lost with Notebook server. Attempting to reconnect..."
|
||||
);
|
||||
}
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
const connectionStatus: ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
};
|
||||
useNotebook.getState().resetConatinerConnection(connectionStatus);
|
||||
useNotebook.getState().setIsRefreshed(true);
|
||||
}
|
||||
this.onConnectionLost();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -212,6 +212,7 @@ export default class NotebookManager {
|
||||
"Cancel",
|
||||
() => reject(new Error("Commit dialog canceled")),
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
label: "Commit message",
|
||||
autoAdjustHeight: true,
|
||||
|
||||
@@ -16,9 +16,10 @@ import "./NotebookReadOnlyRenderer.less";
|
||||
import SandboxOutputs from "./outputs/SandboxOutputs";
|
||||
|
||||
export interface NotebookRendererProps {
|
||||
contentRef: any;
|
||||
contentRef: ContentRef;
|
||||
hideInputs?: boolean;
|
||||
hidePrompts?: boolean;
|
||||
addTransform: (component: React.ComponentType & { MIMETYPE: string }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ export interface NotebookRendererProps {
|
||||
class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
componentDidMount() {
|
||||
if (!userContext.features.sandboxNotebookOutputs) {
|
||||
loadTransform(this.props as any);
|
||||
loadTransform(this.props as NotebookRendererProps);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
<div className="NotebookReadOnlyRender">
|
||||
<Cells contentRef={this.props.contentRef}>
|
||||
{{
|
||||
code: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
code: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||
<CodeCell id={id} contentRef={contentRef}>
|
||||
{{
|
||||
prompt: (props: { id: string; contentRef: string }) => this.renderPrompt(props.id, props.contentRef),
|
||||
@@ -73,14 +74,14 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
}}
|
||||
</CodeCell>
|
||||
),
|
||||
markdown: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
markdown: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||
<MarkdownCell id={id} contentRef={contentRef} cell_type="markdown">
|
||||
{{
|
||||
editor: {},
|
||||
}}
|
||||
</MarkdownCell>
|
||||
),
|
||||
raw: ({ id, contentRef }: { id: any; contentRef: ContentRef }) => (
|
||||
raw: ({ id, contentRef }: { id: string; contentRef: ContentRef }) => (
|
||||
<RawCell id={id} contentRef={contentRef} cell_type="raw">
|
||||
{{
|
||||
editor: {
|
||||
@@ -98,6 +99,7 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||
return {
|
||||
@@ -114,4 +116,4 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
|
||||
return mapDispatchToProps;
|
||||
};
|
||||
|
||||
export default connect(null, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
||||
export default connect(undefined, makeMapDispatchToProps)(NotebookReadOnlyRenderer);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AppState, selectors } from "@nteract/core";
|
||||
import domtoimage from "dom-to-image";
|
||||
import Html2Canvas from "html2canvas";
|
||||
import path from "path";
|
||||
import { userContext } from "../../UserContext";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import * as StringUtils from "../../Utils/StringUtils";
|
||||
import { SnapshotFragment } from "./NotebookComponent/types";
|
||||
@@ -328,4 +329,16 @@ export class NotebookUtil {
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
public static getNotebookBtnTitle(fileName: string): string {
|
||||
if (this.isPhoenixEnabled()) {
|
||||
return `Download to ${fileName}`;
|
||||
} else {
|
||||
return `Download to my notebooks`;
|
||||
}
|
||||
}
|
||||
|
||||
public static isPhoenixEnabled(): boolean {
|
||||
return userContext.features.notebooksTemporarilyDown === false && userContext.features.phoenix === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ import { cloneDeep } from "lodash";
|
||||
import create, { UseStore } from "zustand";
|
||||
import { AuthType } from "../../AuthType";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
import { ConnectionStatusType } from "../../Common/Constants";
|
||||
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
||||
import * as Logger from "../../Common/Logger";
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { ContainerConnectionInfo } from "../../Contracts/DataModels";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||
@@ -14,6 +16,7 @@ import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||
import NotebookManager from "./NotebookManager";
|
||||
import { NotebookUtil } from "./NotebookUtil";
|
||||
|
||||
interface NotebookState {
|
||||
isNotebookEnabled: boolean;
|
||||
@@ -28,8 +31,10 @@ interface NotebookState {
|
||||
myNotebooksContentRoot: NotebookContentItem;
|
||||
gitHubNotebooksContentRoot: NotebookContentItem;
|
||||
galleryContentRoot: NotebookContentItem;
|
||||
connectionInfo: DataModels.ContainerConnectionInfo;
|
||||
NotebookFolderName: string;
|
||||
connectionInfo: ContainerConnectionInfo;
|
||||
notebookFolderName: string;
|
||||
isAllocating: boolean;
|
||||
isRefreshed: boolean;
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => void;
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => void;
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) => void;
|
||||
@@ -38,6 +43,7 @@ interface NotebookState {
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => void;
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => void;
|
||||
setNotebookBasePath: (notebookBasePath: string) => void;
|
||||
setNotebookFolderName: (notebookFolderName: string) => void;
|
||||
refreshNotebooksEnabledStateForAccount: () => Promise<void>;
|
||||
findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem;
|
||||
insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
@@ -45,7 +51,10 @@ interface NotebookState {
|
||||
deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void;
|
||||
initializeNotebooksTree: (notebookManager: NotebookManager) => Promise<void>;
|
||||
initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void;
|
||||
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => void;
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void;
|
||||
setIsAllocating: (isAllocating: boolean) => void;
|
||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo) => void;
|
||||
setIsRefreshed: (isAllocating: boolean) => void;
|
||||
}
|
||||
|
||||
export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
@@ -68,8 +77,12 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
myNotebooksContentRoot: undefined,
|
||||
gitHubNotebooksContentRoot: undefined,
|
||||
galleryContentRoot: undefined,
|
||||
connectionInfo: undefined,
|
||||
NotebookFolderName: userContext.features.phoenix ? "My Notebooks Scratch" : "My Notebooks",
|
||||
connectionInfo: {
|
||||
status: ConnectionStatusType.Connect,
|
||||
},
|
||||
notebookFolderName: undefined,
|
||||
isAllocating: false,
|
||||
isRefreshed: false,
|
||||
setIsNotebookEnabled: (isNotebookEnabled: boolean) => set({ isNotebookEnabled }),
|
||||
setIsNotebooksEnabledForAccount: (isNotebooksEnabledForAccount: boolean) => set({ isNotebooksEnabledForAccount }),
|
||||
setNotebookServerInfo: (notebookServerInfo: DataModels.NotebookWorkspaceConnectionInfo) =>
|
||||
@@ -80,6 +93,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
setMemoryUsageInfo: (memoryUsageInfo: DataModels.MemoryUsageInfo) => set({ memoryUsageInfo }),
|
||||
setIsShellEnabled: (isShellEnabled: boolean) => set({ isShellEnabled }),
|
||||
setNotebookBasePath: (notebookBasePath: string) => set({ notebookBasePath }),
|
||||
setNotebookFolderName: (notebookFolderName: string) => set({ notebookFolderName }),
|
||||
refreshNotebooksEnabledStateForAccount: async (): Promise<void> => {
|
||||
const { databaseAccount, authType } = userContext;
|
||||
if (
|
||||
@@ -173,8 +187,10 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root });
|
||||
},
|
||||
initializeNotebooksTree: async (notebookManager: NotebookManager): Promise<void> => {
|
||||
const notebookFolderName = NotebookUtil.isPhoenixEnabled() === true ? "Temporary Notebooks" : "My Notebooks";
|
||||
set({ notebookFolderName });
|
||||
const myNotebooksContentRoot = {
|
||||
name: get().NotebookFolderName,
|
||||
name: get().notebookFolderName,
|
||||
path: get().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
@@ -183,11 +199,14 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
path: "Gallery",
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
const gitHubNotebooksContentRoot = {
|
||||
name: "GitHub repos",
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn()
|
||||
? {
|
||||
name: "GitHub repos",
|
||||
path: "PsuedoDir",
|
||||
type: NotebookContentItemType.Directory,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
set({
|
||||
myNotebooksContentRoot,
|
||||
galleryContentRoot,
|
||||
@@ -249,5 +268,15 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
|
||||
set({ gitHubNotebooksContentRoot });
|
||||
}
|
||||
},
|
||||
setConnectionInfo: (connectionInfo: DataModels.ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }),
|
||||
setIsAllocating: (isAllocating: boolean) => set({ isAllocating }),
|
||||
resetConatinerConnection: (connectionStatus: ContainerConnectionInfo): void => {
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
useNotebook.getState().setNotebookServerInfo({
|
||||
notebookServerEndpoint: undefined,
|
||||
authToken: undefined,
|
||||
});
|
||||
useNotebook.getState().setIsAllocating(false);
|
||||
},
|
||||
setIsRefreshed: (isRefreshed: boolean) => set({ isRefreshed }),
|
||||
}));
|
||||
|
||||
@@ -161,7 +161,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()}.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -210,6 +210,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
className="panelTextField"
|
||||
aria-label="New database id"
|
||||
autoFocus
|
||||
tabIndex={0}
|
||||
value={this.state.newDatabaseId}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
this.setState({ newDatabaseId: event.target.value })
|
||||
@@ -236,7 +237,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
true
|
||||
).toLocaleLowerCase()} within the database.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -279,7 +280,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={`Unique identifier for the ${getCollectionName().toLocaleLowerCase()} and used for id-based routing through REST and all SDKs.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -362,7 +363,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
"Sharded collections split your data across many replica sets (shards) to achieve unlimited scalability. Sharded collections require choosing a shard key (field) to evenly distribute your data."
|
||||
}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -409,7 +410,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getPartitionKeyTooltipText()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -467,7 +468,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
does not count towards the throughput you provisioned for the database. This throughput amount will be
|
||||
billed in addition to the throughput amount you provisioned at the database level.`}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
)}
|
||||
@@ -497,7 +498,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
creating a unique key policy when a container is created, you ensure the uniqueness of one or more values
|
||||
per partition key."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -560,7 +561,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content={this.getAnalyticalStorageTooltipContent()}
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -637,7 +638,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
directionalHint={DirectionalHint.bottomLeftEdge}
|
||||
content="The _id field is indexed by default. Creating a wildcard index for all fields will optimize queries and is recommended for development."
|
||||
>
|
||||
<Icon iconName="Info" className="panelInfoIcon" />
|
||||
<Icon iconName="Info" className="panelInfoIcon" tabIndex={0} />
|
||||
</TooltipHost>
|
||||
</Stack>
|
||||
|
||||
@@ -998,7 +999,7 @@ export class AddCollectionPanel extends React.Component<AddCollectionPanelProps,
|
||||
|
||||
const collectionId: string = this.state.collectionId.trim();
|
||||
let databaseId = this.state.createNewDatabase ? this.state.newDatabaseId.trim() : this.state.selectedDatabaseId;
|
||||
let partitionKeyString = this.state.partitionKey.trim();
|
||||
let partitionKeyString = this.state.isSharded ? this.state.partitionKey.trim() : undefined;
|
||||
|
||||
if (userContext.apiType === "Tables") {
|
||||
// Table require fixed Database: TablesDB, and fixed Partition Key: /'$pk'
|
||||
|
||||
@@ -23,10 +23,12 @@ import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneFor
|
||||
|
||||
export interface AddDatabasePaneProps {
|
||||
explorer: Explorer;
|
||||
buttonElement?: HTMLElement;
|
||||
}
|
||||
|
||||
export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
explorer: container,
|
||||
buttonElement,
|
||||
}: AddDatabasePaneProps) => {
|
||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||
let throughput: number;
|
||||
@@ -77,6 +79,7 @@ export const AddDatabasePanel: FunctionComponent<AddDatabasePaneProps> = ({
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
};
|
||||
TelemetryProcessor.trace(Action.CreateDatabase, ActionModifiers.Open, addDatabasePaneOpenMessage);
|
||||
buttonElement.focus();
|
||||
}, []);
|
||||
|
||||
const onSubmit = () => {
|
||||
|
||||
@@ -198,6 +198,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
<Stack className="panelGroupSpacing">
|
||||
<TextField
|
||||
aria-required="true"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
styles={getTextFieldStyles()}
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
@@ -285,6 +286,7 @@ export const CassandraAddCollectionPane: FunctionComponent<CassandraAddCollectio
|
||||
underlined
|
||||
styles={getTextFieldStyles({ fontSize: 12, width: 150 })}
|
||||
aria-required="true"
|
||||
required={true}
|
||||
ariaLabel="addCollection-tableId"
|
||||
autoComplete="off"
|
||||
pattern="[^/?#\\]*[^/?# \\]"
|
||||
|
||||
@@ -77,7 +77,7 @@ export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
} else if (selectedLocation.type === "MyNotebooks" && userContext.features.phoenix) {
|
||||
destination = "My Notebooks Scratch";
|
||||
destination = useNotebook.getState().notebookFolderName;
|
||||
}
|
||||
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||
|
||||
@@ -49,8 +49,8 @@ export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps>
|
||||
const options: IDropdownOption[] = [];
|
||||
options.push({
|
||||
key: "MyNotebooks-Item",
|
||||
text: useNotebook.getState().NotebookFolderName,
|
||||
title: useNotebook.getState().NotebookFolderName,
|
||||
text: useNotebook.getState().notebookFolderName,
|
||||
title: useNotebook.getState().notebookFolderName,
|
||||
data: {
|
||||
type: "MyNotebooks",
|
||||
} as Location,
|
||||
|
||||
@@ -108,6 +108,8 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
submitButtonText: "OK",
|
||||
onSubmit,
|
||||
};
|
||||
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
||||
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
<div className="panelFormWrapper">
|
||||
@@ -123,6 +125,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setInputCollectionName(newInput);
|
||||
}}
|
||||
ariaLabel={confirmContainer}
|
||||
/>
|
||||
</div>
|
||||
{shouldRecordFeedback() && (
|
||||
@@ -142,6 +145,7 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDeleteCollectionFeedback(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -40,6 +40,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
id="confirmCollectionId"
|
||||
onChange={[Function]}
|
||||
@@ -53,6 +54,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
value=""
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmCollectionId"
|
||||
@@ -346,6 +348,7 @@ exports[`Delete Collection Confirmation Pane submit() should call delete collect
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the container id"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-57"
|
||||
id="confirmCollectionId"
|
||||
|
||||
@@ -118,7 +118,8 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
message:
|
||||
"Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.",
|
||||
};
|
||||
|
||||
const confirmDatabase = "Confirm by typing the database id";
|
||||
const reasonInfo = "Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?";
|
||||
return (
|
||||
<RightPaneForm {...props}>
|
||||
{!formError && <PanelInfoErrorComponent {...errorProps} />}
|
||||
@@ -133,6 +134,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseInput(newInput);
|
||||
}}
|
||||
ariaLabel={confirmDatabase}
|
||||
/>
|
||||
</div>
|
||||
{isLastNonEmptyDatabase() && (
|
||||
@@ -151,6 +153,7 @@ export const DeleteDatabaseConfirmationPanel: FunctionComponent<DeleteDatabaseCo
|
||||
onChange={(event, newInput?: string) => {
|
||||
setDatabaseFeedbackInput(newInput);
|
||||
}}
|
||||
ariaLabel={reasonInfo}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -153,7 +153,7 @@ export const ExecuteSprocParamsPane: FunctionComponent<ExecuteSprocParamsPanePro
|
||||
selectedKey={paramKeyValue && paramKeyValue.key}
|
||||
/>
|
||||
))}
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex}>
|
||||
<Stack horizontal onClick={addNewParamAtLastIndex} tabIndex={0}>
|
||||
<Image {...imageProps} src={AddPropertyIcon} alt="Add param" />
|
||||
<Text className="addNewParamStyle">Add New Param</Text>
|
||||
</Stack>
|
||||
|
||||
@@ -59,30 +59,36 @@ export const InputParameter: FunctionComponent<InputParameterProps> = ({
|
||||
onChange={onParamKeyChange}
|
||||
options={options}
|
||||
styles={dropdownStyles}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<TextField
|
||||
label={inputLabel && inputLabel}
|
||||
id="confirmCollectionId"
|
||||
autoFocus
|
||||
value={paramValue}
|
||||
onChange={onParamValueChange}
|
||||
/>
|
||||
{isAddRemoveVisible && (
|
||||
<>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
id="deleteparam"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
/>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
id="addparam"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
/>
|
||||
<div tabIndex={0}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={EntityCancelIcon}
|
||||
alt="Delete param"
|
||||
id="deleteparam"
|
||||
role="button"
|
||||
onClick={onDeleteParamKeyPress}
|
||||
/>
|
||||
</div>
|
||||
<div tabIndex={0}>
|
||||
<Image
|
||||
{...imageProps}
|
||||
src={AddPropertyIcon}
|
||||
alt="Add param"
|
||||
id="addparam"
|
||||
role="button"
|
||||
onClick={onAddNewParamKeyPress}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,13 @@
|
||||
.panelFormWrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
|
||||
.panelMainContent {
|
||||
flex-grow: 1;
|
||||
padding: 0 34px;
|
||||
margin: 20px 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
& > :not(.collapsibleSection) {
|
||||
margin-bottom: @DefaultSpace;
|
||||
|
||||
@@ -33,7 +33,13 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
<Stack className="panelInfoErrorContainer" horizontal verticalAlign="center">
|
||||
{icon}
|
||||
<span className="panelWarningErrorDetailsLinkContainer">
|
||||
<Text className="panelWarningErrorMessage" variant="small" aria-label="message">
|
||||
<Text
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-label={message}
|
||||
className="panelWarningErrorMessage"
|
||||
variant="small"
|
||||
>
|
||||
{message}
|
||||
{link && linkText && (
|
||||
<Link target="_blank" href={link}>
|
||||
|
||||
@@ -34,6 +34,6 @@ describe("Right Pane Form", () => {
|
||||
it("should render error in header", () => {
|
||||
render(<RightPaneForm {...props} formError="file already Exist" />);
|
||||
expect(screen.getByLabelText("error")).toBeDefined();
|
||||
expect(screen.getByLabelText("message").innerHTML).toEqual("file already Exist");
|
||||
expect(screen.getByRole("alert").innerHTML).toEqual("file already Exist");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -195,7 +195,6 @@ export const SettingsPane: FunctionComponent = () => {
|
||||
step={1}
|
||||
className="textfontclr"
|
||||
role="textbox"
|
||||
tabIndex={0}
|
||||
id="max-degree"
|
||||
value={"" + maxDegreeOfParallelism}
|
||||
onIncrement={(newValue) => setMaxDegreeOfParallelism(parseInt(newValue) + 1 || maxDegreeOfParallelism)}
|
||||
|
||||
@@ -123,7 +123,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
onValidate={[Function]}
|
||||
role="textbox"
|
||||
step={1}
|
||||
tabIndex={0}
|
||||
value="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ describe("Table query select Panel", () => {
|
||||
it("Should checked availableCheckbox by default", () => {
|
||||
const wrapper = mount(<TableQuerySelectPanel {...props} />);
|
||||
expect(wrapper.find("#availableCheckbox").first().props()).toEqual({
|
||||
ariaPositionInSet: 0,
|
||||
id: "availableCheckbox",
|
||||
label: "Available Columns",
|
||||
checked: true,
|
||||
|
||||
@@ -128,8 +128,9 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
label="Available Columns"
|
||||
checked={isAvailableColumnChecked}
|
||||
onChange={availableColumnsCheckboxClick}
|
||||
ariaPositionInSet={0}
|
||||
/>
|
||||
{columnOptions.map((column) => {
|
||||
{columnOptions.map((column, index) => {
|
||||
return (
|
||||
<Checkbox
|
||||
label={column.columnName}
|
||||
@@ -137,6 +138,7 @@ export const TableQuerySelectPanel: FunctionComponent<TableQuerySelectPanelProps
|
||||
key={column.columnName}
|
||||
checked={column.selected}
|
||||
disabled={!column.editable}
|
||||
ariaPositionInSet={index + 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -37,12 +37,14 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
className="column-select-view"
|
||||
>
|
||||
<StyledCheckboxBase
|
||||
ariaPositionInSet={0}
|
||||
checked={true}
|
||||
id="availableCheckbox"
|
||||
label="Available Columns"
|
||||
onChange={[Function]}
|
||||
>
|
||||
<CheckboxBase
|
||||
ariaPositionInSet={0}
|
||||
checked={true}
|
||||
id="availableCheckbox"
|
||||
label="Available Columns"
|
||||
@@ -328,6 +330,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
<input
|
||||
aria-checked="true"
|
||||
aria-label="Available Columns"
|
||||
aria-posinset={0}
|
||||
checked={true}
|
||||
className="input-55"
|
||||
data-ktp-execute-target={true}
|
||||
@@ -646,6 +649,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
</CheckboxBase>
|
||||
</StyledCheckboxBase>
|
||||
<StyledCheckboxBase
|
||||
ariaPositionInSet={1}
|
||||
checked={true}
|
||||
disabled={false}
|
||||
key=""
|
||||
@@ -653,6 +657,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
onChange={[Function]}
|
||||
>
|
||||
<CheckboxBase
|
||||
ariaPositionInSet={1}
|
||||
checked={true}
|
||||
disabled={false}
|
||||
label=""
|
||||
@@ -939,6 +944,7 @@ exports[`Table query select Panel should render Default properly 1`] = `
|
||||
aria-checked="true"
|
||||
aria-disabled={false}
|
||||
aria-label=""
|
||||
aria-posinset={1}
|
||||
checked={true}
|
||||
className="input-55"
|
||||
data-ktp-execute-target={true}
|
||||
|
||||
@@ -327,13 +327,17 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
key=".0:$.1"
|
||||
>
|
||||
<Text
|
||||
aria-label="message"
|
||||
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||
aria-live="assertive"
|
||||
className="panelWarningErrorMessage"
|
||||
role="alert"
|
||||
variant="small"
|
||||
>
|
||||
<span
|
||||
aria-label="message"
|
||||
aria-label="Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources."
|
||||
aria-live="assertive"
|
||||
className="panelWarningErrorMessage css-56"
|
||||
role="alert"
|
||||
>
|
||||
Warning! The action you are about to take cannot be undone. Continuing will permanently delete this resource and all of its children resources.
|
||||
</span>
|
||||
@@ -363,6 +367,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Confirm by typing the database id"
|
||||
autoFocus={true}
|
||||
id="confirmDatabaseId"
|
||||
onChange={[Function]}
|
||||
@@ -375,6 +380,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Confirm by typing the database id"
|
||||
autoFocus={true}
|
||||
deferredValidationTime={200}
|
||||
id="confirmDatabaseId"
|
||||
@@ -667,6 +673,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-invalid={false}
|
||||
aria-label="Confirm by typing the database id"
|
||||
autoFocus={true}
|
||||
className="ms-TextField-field field-60"
|
||||
id="confirmDatabaseId"
|
||||
@@ -707,6 +714,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
</span>
|
||||
</Text>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
@@ -720,6 +728,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
}
|
||||
>
|
||||
<TextFieldBase
|
||||
ariaLabel="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
deferredValidationTime={200}
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
multiline={true}
|
||||
@@ -1013,6 +1022,7 @@ exports[`Delete Database Confirmation Pane Should call delete database 1`] = `
|
||||
>
|
||||
<textarea
|
||||
aria-invalid={false}
|
||||
aria-label="Help us improve Azure Cosmos DB! What is the reason why you are deleting this database?"
|
||||
className="ms-TextField-field field-71"
|
||||
id="deleteDatabaseFeedbackInput"
|
||||
onBlur={[Function]}
|
||||
|
||||
@@ -307,16 +307,23 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
||||
iconSrc: AddDatabaseIcon,
|
||||
title: "New " + getDatabaseName(),
|
||||
description: undefined,
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel("New " + getDatabaseName(), <AddDatabasePanel explorer={this.container} />),
|
||||
onClick: () => this.openAddDatabasePanel(),
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private openAddDatabasePanel() {
|
||||
const newDatabaseButton = document.activeElement as HTMLElement;
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"New " + getDatabaseName(),
|
||||
<AddDatabasePanel explorer={this.container} buttonElement={newDatabaseButton} />
|
||||
);
|
||||
}
|
||||
|
||||
private decorateOpenCollectionActivity({ databaseId, collectionId }: MostRecentActivity.OpenCollectionItem) {
|
||||
return {
|
||||
iconSrc: NotebookIcon,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
import * as Utilities from "../Utilities";
|
||||
import QueryClauseViewModel from "./QueryClauseViewModel";
|
||||
|
||||
export default class ClauseGroup {
|
||||
public isRootGroup: boolean;
|
||||
//eslint-disable-next-line
|
||||
public children = new Array();
|
||||
public parentGroup: ClauseGroup;
|
||||
private _id: string;
|
||||
@@ -17,7 +18,7 @@ export default class ClauseGroup {
|
||||
* Flattens the clause tree into an array, depth-first, left to right.
|
||||
*/
|
||||
public flattenClauses(targetArray: ko.ObservableArray<QueryClauseViewModel>): void {
|
||||
var tempArray = new Array<QueryClauseViewModel>();
|
||||
const tempArray = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, tempArray);
|
||||
targetArray.removeAll();
|
||||
@@ -31,10 +32,10 @@ export default class ClauseGroup {
|
||||
newClause.clauseGroup = this;
|
||||
this.children.push(newClause);
|
||||
} else {
|
||||
var targetGroup = insertBefore.clauseGroup;
|
||||
const targetGroup = insertBefore.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
|
||||
const insertBeforeIndex = targetGroup.children.indexOf(insertBefore);
|
||||
newClause.clauseGroup = targetGroup;
|
||||
targetGroup.children.splice(insertBeforeIndex, 0, newClause);
|
||||
}
|
||||
@@ -42,19 +43,19 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
public deleteClause(clause: QueryClauseViewModel): void {
|
||||
var targetGroup = clause.clauseGroup;
|
||||
const targetGroup = clause.clauseGroup;
|
||||
|
||||
if (targetGroup) {
|
||||
var index = targetGroup.children.indexOf(clause);
|
||||
const index = targetGroup.children.indexOf(clause);
|
||||
targetGroup.children.splice(index, 1);
|
||||
clause.dispose();
|
||||
|
||||
if (targetGroup.children.length <= 1 && !targetGroup.isRootGroup) {
|
||||
var parent = targetGroup.parentGroup;
|
||||
var targetGroupIndex = parent.children.indexOf(targetGroup);
|
||||
const parent = targetGroup.parentGroup;
|
||||
const targetGroupIndex = parent.children.indexOf(targetGroup);
|
||||
|
||||
if (targetGroup.children.length === 1) {
|
||||
var orphan = targetGroup.children.shift();
|
||||
const orphan = targetGroup.children.shift();
|
||||
|
||||
if (orphan instanceof QueryClauseViewModel) {
|
||||
(<QueryClauseViewModel>orphan).clauseGroup = parent;
|
||||
@@ -71,14 +72,14 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
public removeAll(): void {
|
||||
var allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
|
||||
const allClauses: QueryClauseViewModel[] = new Array<QueryClauseViewModel>();
|
||||
|
||||
this.flattenClausesImpl(this, allClauses);
|
||||
|
||||
while (allClauses.length > 0) {
|
||||
allClauses.shift().dispose();
|
||||
}
|
||||
|
||||
//eslint-disable-next-line
|
||||
this.children = new Array<any>();
|
||||
}
|
||||
|
||||
@@ -87,12 +88,12 @@ export default class ClauseGroup {
|
||||
*/
|
||||
public groupSelectedItems(): boolean {
|
||||
// Find the selection start & end, also check for gaps between selected items (if found, cannot proceed).
|
||||
var selection = this.getCheckedItemsInfo();
|
||||
const selection = this.getCheckedItemsInfo();
|
||||
|
||||
if (selection.canGroup) {
|
||||
var newGroup = new ClauseGroup(false, this);
|
||||
const newGroup = new ClauseGroup(false, this);
|
||||
// Replace the selected items with the new group, and then move the selected items into the new group.
|
||||
var groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
|
||||
const groupedItems = this.children.splice(selection.begin, selection.end - selection.begin + 1, newGroup);
|
||||
|
||||
groupedItems &&
|
||||
groupedItems.forEach((element) => {
|
||||
@@ -118,13 +119,13 @@ export default class ClauseGroup {
|
||||
return;
|
||||
}
|
||||
|
||||
var parentGroup = this.parentGroup;
|
||||
var index = parentGroup.children.indexOf(this);
|
||||
const parentGroup = this.parentGroup;
|
||||
let index = parentGroup.children.indexOf(this);
|
||||
|
||||
if (index >= 0) {
|
||||
parentGroup.children.splice(index, 1);
|
||||
|
||||
var toPromote = this.children.splice(0, this.children.length);
|
||||
const toPromote = this.children.splice(0, this.children.length);
|
||||
|
||||
// Move all children one level up.
|
||||
toPromote &&
|
||||
@@ -146,16 +147,16 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
public findDeepestGroupInChildren(skipIndex?: number): ClauseGroup {
|
||||
var deepest: ClauseGroup = this;
|
||||
var level: number = 0;
|
||||
var func = (currentGroup: ClauseGroup): void => {
|
||||
let deepest = <ClauseGroup>this;
|
||||
let level = 0;
|
||||
const func = (currentGroup: ClauseGroup): void => {
|
||||
level++;
|
||||
if (currentGroup.getCurrentGroupDepth() > deepest.getCurrentGroupDepth()) {
|
||||
deepest = currentGroup;
|
||||
}
|
||||
|
||||
for (var i = 0; i < currentGroup.children.length; i++) {
|
||||
var currentItem = currentGroup.children[i];
|
||||
for (let i = 0; i < currentGroup.children.length; i++) {
|
||||
const currentItem = currentGroup.children[i];
|
||||
|
||||
if ((i !== skipIndex || level > 1) && currentItem instanceof ClauseGroup) {
|
||||
func(currentItem);
|
||||
@@ -170,16 +171,16 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
private getCheckedItemsInfo(): { canGroup: boolean; begin: number; end: number } {
|
||||
var beginIndex = -1;
|
||||
var endIndex = -1;
|
||||
let beginIndex = -1;
|
||||
let endIndex = -1;
|
||||
// In order to perform group, all selected items must be next to each other.
|
||||
// If one or more items are not selected between the first and the last selected item, the gapFlag will be set to True, meaning cannot perform group.
|
||||
var gapFlag = false;
|
||||
var count = 0;
|
||||
let gapFlag = false;
|
||||
let count = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
var subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const currentItem = this.children[i];
|
||||
let subGroupSelectionState: { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean };
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
subGroupSelectionState = (<ClauseGroup>currentItem).getSelectionState();
|
||||
@@ -235,10 +236,10 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
private getSelectionState(): { allSelected: boolean; partiallySelected: boolean; nonSelected: boolean } {
|
||||
var selectedCount = 0;
|
||||
let selectedCount = 0;
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup && (<ClauseGroup>currentItem).getSelectionState().allSelected) {
|
||||
selectedCount++;
|
||||
@@ -260,8 +261,8 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
private unselectAll(): void {
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
(<ClauseGroup>currentItem).unselectAll();
|
||||
@@ -278,8 +279,8 @@ export default class ClauseGroup {
|
||||
targetArray.splice(0, targetArray.length);
|
||||
}
|
||||
|
||||
for (var i = 0; i < queryGroup.children.length; i++) {
|
||||
var currentItem = queryGroup.children[i];
|
||||
for (let i = 0; i < queryGroup.children.length; i++) {
|
||||
const currentItem = queryGroup.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
this.flattenClausesImpl(currentItem, targetArray);
|
||||
@@ -292,13 +293,13 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
public getTreeDepth(): number {
|
||||
var currentDepth = this.getCurrentGroupDepth();
|
||||
let currentDepth = this.getCurrentGroupDepth();
|
||||
|
||||
for (var i = 0; i < this.children.length; i++) {
|
||||
var currentItem = this.children[i];
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
const currentItem = this.children[i];
|
||||
|
||||
if (currentItem instanceof ClauseGroup) {
|
||||
var newDepth = (<ClauseGroup>currentItem).getTreeDepth();
|
||||
const newDepth = (<ClauseGroup>currentItem).getTreeDepth();
|
||||
|
||||
if (newDepth > currentDepth) {
|
||||
currentDepth = newDepth;
|
||||
@@ -310,8 +311,8 @@ export default class ClauseGroup {
|
||||
}
|
||||
|
||||
public getCurrentGroupDepth(): number {
|
||||
var group = <ClauseGroup>this;
|
||||
var depth = 0;
|
||||
let group = <ClauseGroup>this;
|
||||
let depth = 0;
|
||||
|
||||
while (!group.isRootGroup) {
|
||||
depth++;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../Constants";
|
||||
import ClauseGroup from "./ClauseGroup";
|
||||
import QueryBuilderViewModel from "./QueryBuilderViewModel";
|
||||
import * as Constants from "../Constants";
|
||||
|
||||
/**
|
||||
* View model for showing group indicators on UI, contains information such as group color and border styles.
|
||||
@@ -38,7 +38,7 @@ export default class ClauseGroupViewModel {
|
||||
};
|
||||
|
||||
private getGroupBackgroundColor(group: ClauseGroup): string {
|
||||
var colorCount = Constants.clauseGroupColors.length;
|
||||
const colorCount = Constants.clauseGroupColors.length;
|
||||
|
||||
if (group.isRootGroup) {
|
||||
return Constants.transparentColor;
|
||||
|
||||
@@ -29,7 +29,7 @@ export default class QueryBuilderViewModel {
|
||||
public removeThisFilterLine = "Remove this filter line"; // localize
|
||||
public groupSelectedClauses = "Group selected clauses"; // localize
|
||||
public clauseArray = ko.observableArray<QueryClauseViewModel>(); // This is for storing the clauses in flattened form queryClauses for easier UI data binding.
|
||||
public queryClauses = new ClauseGroup(true, null); // The actual data structure containing the clause information.
|
||||
public queryClauses = new ClauseGroup(true, undefined); // The actual data structure containing the clause information.
|
||||
public columnOptions: ko.ObservableArray<string>;
|
||||
public canGroupClauses = ko.observable<boolean>(false);
|
||||
|
||||
@@ -107,7 +107,7 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
|
||||
public setExample() {
|
||||
var example1 = new QueryClauseViewModel(
|
||||
const example1 = new QueryClauseViewModel(
|
||||
this,
|
||||
"",
|
||||
"PartitionKey",
|
||||
@@ -121,7 +121,7 @@ export default class QueryBuilderViewModel {
|
||||
//null,
|
||||
true
|
||||
);
|
||||
var example2 = new QueryClauseViewModel(
|
||||
const example2 = new QueryClauseViewModel(
|
||||
this,
|
||||
"And",
|
||||
"RowKey",
|
||||
@@ -140,13 +140,13 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
|
||||
public getODataFilterFromClauses = (): string => {
|
||||
var filterString: string = "";
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
let filterString = "";
|
||||
const treeTraversal = (group: ClauseGroup): void => {
|
||||
for (let i = 0; i < group.children.length; i++) {
|
||||
const currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
const clause = <QueryClauseViewModel>currentItem;
|
||||
this.timestampToValue(clause);
|
||||
filterString = filterString.concat(
|
||||
this.constructODataClause(
|
||||
@@ -173,7 +173,7 @@ export default class QueryBuilderViewModel {
|
||||
};
|
||||
|
||||
public getSqlFilterFromClauses = (): string => {
|
||||
var filterString: string = "SELECT * FROM c";
|
||||
let filterString = "SELECT * FROM c";
|
||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||
filterString = "SELECT";
|
||||
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
|
||||
@@ -199,15 +199,15 @@ export default class QueryBuilderViewModel {
|
||||
return filterString;
|
||||
}
|
||||
filterString = filterString.concat(" WHERE");
|
||||
var first = true;
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
let first = true;
|
||||
const treeTraversal = (group: ClauseGroup): void => {
|
||||
for (let i = 0; i < group.children.length; i++) {
|
||||
const currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
||||
var value = clause.value();
|
||||
const clause = <QueryClauseViewModel>currentItem;
|
||||
const timeStampValue: string = this.timestampToSqlValue(clause);
|
||||
let value = clause.value();
|
||||
if (!clause.isValue()) {
|
||||
value = timeStampValue;
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export default class QueryBuilderViewModel {
|
||||
const databaseId = this._queryViewModel.queryTablesTab.collection.databaseId;
|
||||
const collectionId = this._queryViewModel.queryTablesTab.collection.id();
|
||||
const tableToQuery = `${getQuotedCqlIdentifier(databaseId)}.${getQuotedCqlIdentifier(collectionId)}`;
|
||||
var filterString: string = `SELECT * FROM ${tableToQuery}`;
|
||||
let filterString = `SELECT * FROM ${tableToQuery}`;
|
||||
if (this._queryViewModel.selectText() && this._queryViewModel.selectText().length > 0) {
|
||||
filterString = "SELECT";
|
||||
const selectText = this._queryViewModel && this._queryViewModel.selectText && this._queryViewModel.selectText();
|
||||
@@ -255,15 +255,15 @@ export default class QueryBuilderViewModel {
|
||||
return filterString;
|
||||
}
|
||||
filterString = filterString.concat(" WHERE");
|
||||
var first = true;
|
||||
var treeTraversal = (group: ClauseGroup): void => {
|
||||
for (var i = 0; i < group.children.length; i++) {
|
||||
var currentItem = group.children[i];
|
||||
let first = true;
|
||||
const treeTraversal = (group: ClauseGroup): void => {
|
||||
for (let i = 0; i < group.children.length; i++) {
|
||||
const currentItem = group.children[i];
|
||||
|
||||
if (currentItem instanceof QueryClauseViewModel) {
|
||||
var clause = <QueryClauseViewModel>currentItem;
|
||||
let timeStampValue: string = this.timestampToSqlValue(clause);
|
||||
var value = clause.value();
|
||||
const clause = <QueryClauseViewModel>currentItem;
|
||||
const timeStampValue = this.timestampToSqlValue(clause);
|
||||
let value = clause.value();
|
||||
if (!clause.isValue()) {
|
||||
value = timeStampValue;
|
||||
}
|
||||
@@ -293,13 +293,13 @@ export default class QueryBuilderViewModel {
|
||||
};
|
||||
|
||||
public updateColumnOptions = (): void => {
|
||||
let originalHeaders = this.columnOptions();
|
||||
let newHeaders = this.tableEntityListViewModel.headers;
|
||||
// let originalHeaders = this.columnOptions();
|
||||
const newHeaders = this.tableEntityListViewModel.headers;
|
||||
this.columnOptions(newHeaders.sort(DataTableUtilities.compareTableColumns));
|
||||
};
|
||||
|
||||
private generateLeftParentheses(clause: QueryClauseViewModel): string {
|
||||
var result = "";
|
||||
let result = "";
|
||||
|
||||
if (clause.clauseGroup.isRootGroup || clause.clauseGroup.children.indexOf(clause) !== 0) {
|
||||
return result;
|
||||
@@ -307,7 +307,7 @@ export default class QueryBuilderViewModel {
|
||||
result = result.concat("(");
|
||||
}
|
||||
|
||||
var currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
let currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
|
||||
while (
|
||||
!currentGroup.isRootGroup &&
|
||||
@@ -322,7 +322,7 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
|
||||
private generateRightParentheses(clause: QueryClauseViewModel): string {
|
||||
var result = "";
|
||||
let result = "";
|
||||
|
||||
if (
|
||||
clause.clauseGroup.isRootGroup ||
|
||||
@@ -333,7 +333,7 @@ export default class QueryBuilderViewModel {
|
||||
result = result.concat(")");
|
||||
}
|
||||
|
||||
var currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
let currentGroup: ClauseGroup = clause.clauseGroup;
|
||||
|
||||
while (
|
||||
!currentGroup.isRootGroup &&
|
||||
@@ -364,14 +364,17 @@ export default class QueryBuilderViewModel {
|
||||
case Constants.TableType.String:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
)} \'${value}\'${rightParentheses}`;
|
||||
case Constants.TableType.Guid:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
)} guid\'${value}\'${rightParentheses}`;
|
||||
case Constants.TableType.Binary:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
operator
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
)} binary\'${value}\'${rightParentheses}`;
|
||||
default:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}${propertyName} ${this.operatorConverter(
|
||||
@@ -391,9 +394,11 @@ export default class QueryBuilderViewModel {
|
||||
): string => {
|
||||
if (propertyName === Constants.EntityKeyNames.PartitionKey) {
|
||||
propertyName = TableEntityProcessor.keyProperties.PartitionKey;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c["${propertyName}"] ${operator} \'${value}\'${rightParentheses}`;
|
||||
} else if (propertyName === Constants.EntityKeyNames.RowKey) {
|
||||
propertyName = TableEntityProcessor.keyProperties.Id;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName} ${operator} \'${value}\'${rightParentheses}`;
|
||||
} else if (propertyName === Constants.EntityKeyNames.Timestamp) {
|
||||
propertyName = TableEntityProcessor.keyProperties.Timestamp;
|
||||
@@ -403,16 +408,21 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
switch (type) {
|
||||
case Constants.TableType.DateTime:
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${DateTimeUtilities.convertJSDateToTicksWithPadding(
|
||||
value
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
)}\'${rightParentheses}`;
|
||||
case Constants.TableType.Int64:
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${Utilities.padLongWithZeros(
|
||||
value
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
)}\'${rightParentheses}`;
|
||||
case Constants.TableType.String:
|
||||
case Constants.TableType.Guid:
|
||||
case Constants.TableType.Binary:
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} \'${value}\'${rightParentheses}`;
|
||||
default:
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses}c.${propertyName}["$v"] ${operator} ${value}${rightParentheses}`;
|
||||
@@ -434,6 +444,7 @@ export default class QueryBuilderViewModel {
|
||||
type === Constants.CassandraType.Ascii ||
|
||||
type === Constants.CassandraType.Varchar
|
||||
) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} \'${value}\'${rightParentheses}`;
|
||||
}
|
||||
return ` ${clauseRule.toLowerCase()} ${leftParentheses} ${propertyName} ${operator} ${value}${rightParentheses}`;
|
||||
@@ -454,7 +465,7 @@ export default class QueryBuilderViewModel {
|
||||
case Constants.Operator.NotEqualTo:
|
||||
return Constants.ODataOperator.NotEqualTo;
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
};
|
||||
|
||||
public groupClauses = (): void => {
|
||||
@@ -463,11 +474,11 @@ export default class QueryBuilderViewModel {
|
||||
this.updateCanGroupClauses();
|
||||
};
|
||||
|
||||
public addClauseIndex = (index: number, data: any): void => {
|
||||
public addClauseIndex = (index: number): void => {
|
||||
if (index < 0) {
|
||||
index = 0;
|
||||
}
|
||||
var newClause = new QueryClauseViewModel(
|
||||
const newClause = new QueryClauseViewModel(
|
||||
this,
|
||||
"And",
|
||||
"",
|
||||
@@ -492,28 +503,28 @@ export default class QueryBuilderViewModel {
|
||||
|
||||
// adds a new clause to the end of the array
|
||||
public addNewClause = (): void => {
|
||||
this.addClauseIndex(this.clauseArray().length, null);
|
||||
this.addClauseIndex(this.clauseArray().length);
|
||||
};
|
||||
|
||||
public onAddClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
|
||||
public onAddClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.addClauseIndex(index, data);
|
||||
this.addClauseIndex(index);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public onAddNewClauseKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
public onAddNewClauseKeyDown = (event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.addClauseIndex(this.clauseArray().length - 1, null);
|
||||
this.addClauseIndex(this.clauseArray().length - 1);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
public deleteClause = (index: number, data: any): void => {
|
||||
public deleteClause = (index: number): void => {
|
||||
this.deleteClauseImpl(index);
|
||||
if (this.clauseArray().length !== 0) {
|
||||
this.clauseArray()[0].and_or("");
|
||||
@@ -523,9 +534,9 @@ export default class QueryBuilderViewModel {
|
||||
$(window).resize();
|
||||
};
|
||||
|
||||
public onDeleteClauseKeyDown = (index: number, data: any, event: KeyboardEvent, source: any): boolean => {
|
||||
public onDeleteClauseKeyDown = (index: number, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.deleteClause(index, data);
|
||||
this.deleteClause(index);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
@@ -539,25 +550,26 @@ export default class QueryBuilderViewModel {
|
||||
* (transparent) or its parent group view models.
|
||||
*/
|
||||
public getClauseGroupViewModels = (clause: QueryClauseViewModel): ClauseGroupViewModel[] => {
|
||||
var placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
|
||||
var treeDepth = this.queryClauses.getTreeDepth();
|
||||
var groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
|
||||
const placeHolderGroupViewModel = new ClauseGroupViewModel(this.queryClauses, false, this);
|
||||
const treeDepth = this.queryClauses.getTreeDepth();
|
||||
const groupViewModels = new Array<ClauseGroupViewModel>(treeDepth);
|
||||
|
||||
// Prefill the arry with placeholders.
|
||||
for (var i = 0; i < groupViewModels.length; i++) {
|
||||
for (let i = 0; i < groupViewModels.length; i++) {
|
||||
groupViewModels[i] = placeHolderGroupViewModel;
|
||||
}
|
||||
|
||||
var currentGroup = clause.clauseGroup;
|
||||
let currentGroup = clause.clauseGroup;
|
||||
|
||||
// This function determines whether the path from clause to the current group is on the left most.
|
||||
var isLeftMostPath = (): boolean => {
|
||||
var group = clause.clauseGroup;
|
||||
const isLeftMostPath = (): boolean => {
|
||||
let group = clause.clauseGroup;
|
||||
|
||||
if (group.children.indexOf(clause) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (group.getId() === currentGroup.getId()) {
|
||||
break;
|
||||
@@ -573,13 +585,14 @@ export default class QueryBuilderViewModel {
|
||||
};
|
||||
|
||||
// This function determines whether the path from clause to the current group is on the right most.
|
||||
var isRightMostPath = (): boolean => {
|
||||
var group = clause.clauseGroup;
|
||||
const isRightMostPath = (): boolean => {
|
||||
let group = clause.clauseGroup;
|
||||
|
||||
if (group.children.indexOf(clause) !== group.children.length - 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
if (group.getId() === currentGroup.getId()) {
|
||||
break;
|
||||
@@ -594,26 +607,26 @@ export default class QueryBuilderViewModel {
|
||||
return true;
|
||||
};
|
||||
|
||||
var vmIndex = groupViewModels.length - 1;
|
||||
var skipIndex = -1;
|
||||
var lastDepth = clause.groupDepth;
|
||||
let vmIndex = groupViewModels.length - 1;
|
||||
let skipIndex = -1;
|
||||
let lastDepth = clause.groupDepth;
|
||||
|
||||
while (!currentGroup.isRootGroup) {
|
||||
// The current group will be rendered at least once, and if there are any sibling groups deeper
|
||||
// than the current group, we will repeat rendering the current group to fill up the gap between
|
||||
// current & deepest sibling.
|
||||
var deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
|
||||
const deepestInSiblings = currentGroup.findDeepestGroupInChildren(skipIndex).getCurrentGroupDepth();
|
||||
// Find out the depth difference between the deepest group under the siblings of currentGroup and
|
||||
// the deepest group under currentGroup. If the result n is a positive number, it means there are
|
||||
// deeper groups in siblings and we need to draw n + 1 group blocks on UI to fill up the depth
|
||||
// differences. If the result n is a negative number, it means current group contains the deepest
|
||||
// sub-group, we only need to draw the group block once.
|
||||
var repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
|
||||
const repeatCount = Math.max(deepestInSiblings - lastDepth, 0);
|
||||
|
||||
for (var i = 0; i <= repeatCount; i++) {
|
||||
var isLeftMost = isLeftMostPath();
|
||||
var isRightMost = isRightMostPath();
|
||||
var groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
|
||||
for (let i = 0; i <= repeatCount; i++) {
|
||||
const isLeftMost = isLeftMostPath();
|
||||
const isRightMost = isRightMostPath();
|
||||
const groupViewModel = new ClauseGroupViewModel(currentGroup, i === 0 && isLeftMost, this);
|
||||
|
||||
groupViewModel.showTopBorder(isLeftMost);
|
||||
groupViewModel.showBottomBorder(isRightMost);
|
||||
@@ -635,9 +648,9 @@ export default class QueryBuilderViewModel {
|
||||
};
|
||||
|
||||
public addCustomRange(timestamp: CustomTimestampHelper.ITimestampQuery, clauseToAdd: QueryClauseViewModel): void {
|
||||
var index = this.clauseArray.peek().indexOf(clauseToAdd);
|
||||
const index = this.clauseArray.peek().indexOf(clauseToAdd);
|
||||
|
||||
var newClause = new QueryClauseViewModel(
|
||||
const newClause = new QueryClauseViewModel(
|
||||
this,
|
||||
//this._tableEntityListViewModel.tableExplorerContext.hostProxy,
|
||||
"And",
|
||||
@@ -662,10 +675,10 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
|
||||
private scrollToBottom(): void {
|
||||
var scrollBox = document.getElementById("scroll");
|
||||
const scrollBox = document.getElementById("scroll");
|
||||
if (!this.scrollEventListener) {
|
||||
scrollBox.addEventListener("scroll", function () {
|
||||
var translate = "translate(0," + this.scrollTop + "px)";
|
||||
const translate = "translate(0," + this.scrollTop + "px)";
|
||||
const allTh = <NodeListOf<HTMLElement>>this.querySelectorAll("thead td");
|
||||
for (let i = 0; i < allTh.length; i++) {
|
||||
allTh[i].style.transform = translate;
|
||||
@@ -673,7 +686,7 @@ export default class QueryBuilderViewModel {
|
||||
});
|
||||
this.scrollEventListener = true;
|
||||
}
|
||||
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
|
||||
const isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
|
||||
if (isScrolledToBottom) {
|
||||
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
|
||||
}
|
||||
@@ -685,8 +698,8 @@ export default class QueryBuilderViewModel {
|
||||
}
|
||||
|
||||
private deleteClauseImpl(index: number): void {
|
||||
var clause = this.clauseArray()[index];
|
||||
var previousClause = index === 0 ? 0 : index - 1;
|
||||
const clause = this.clauseArray()[index];
|
||||
const previousClause = index === 0 ? 0 : index - 1;
|
||||
this.queryClauses.deleteClause(clause);
|
||||
this.updateClauseArray();
|
||||
if (this.clauseArray()[previousClause]) {
|
||||
@@ -731,7 +744,7 @@ export default class QueryBuilderViewModel {
|
||||
|
||||
private timestampToSqlValue(clause: QueryClauseViewModel): string {
|
||||
if (clause.isValue()) {
|
||||
return null;
|
||||
return undefined;
|
||||
} else if (clause.isTimestamp()) {
|
||||
return this.getTimeStampToSqlQuery(clause);
|
||||
// } else if (clause.isCustomLastTimestamp()) {
|
||||
@@ -743,7 +756,7 @@ export default class QueryBuilderViewModel {
|
||||
return clause.customTimeValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getTimeStampToQuery(clause: QueryClauseViewModel): void {
|
||||
@@ -789,7 +802,7 @@ export default class QueryBuilderViewModel {
|
||||
case Constants.timeOptions.currentYear:
|
||||
return CustomTimestampHelper._queryCurrentYearLocal();
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public checkIfClauseChanged(): void {
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class QueryClauseViewModel {
|
||||
public field: ko.Observable<string>;
|
||||
public type: ko.Observable<string>;
|
||||
public operator: ko.Observable<string>;
|
||||
public value: ko.Observable<any>;
|
||||
public value: ko.Observable<string>;
|
||||
public timeValue: ko.Observable<string>;
|
||||
public customTimeValue: ko.Observable<string>;
|
||||
public canAnd: ko.Observable<boolean>;
|
||||
@@ -39,7 +39,7 @@ export default class QueryClauseViewModel {
|
||||
field: string,
|
||||
type: string,
|
||||
operator: string,
|
||||
value: any,
|
||||
value: string,
|
||||
canAnd: boolean,
|
||||
timeValue: string,
|
||||
customTimeValue: string,
|
||||
@@ -88,30 +88,30 @@ export default class QueryClauseViewModel {
|
||||
userContext.apiType !== "Cassandra"
|
||||
);
|
||||
|
||||
this.and_or.subscribe((value) => {
|
||||
this.and_or.subscribe(() => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.field.subscribe((value) => {
|
||||
this.field.subscribe(() => {
|
||||
this.changeField();
|
||||
});
|
||||
this.type.subscribe((value) => {
|
||||
this.type.subscribe(() => {
|
||||
this.changeType();
|
||||
});
|
||||
this.timeValue.subscribe((value) => {
|
||||
this.timeValue.subscribe(() => {
|
||||
// if (this.timeValue() === QueryBuilderConstants.timeOptions.custom) {
|
||||
// this.customTimestampDialog();
|
||||
// }
|
||||
});
|
||||
this.customTimeValue.subscribe((value) => {
|
||||
this.customTimeValue.subscribe(() => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.value.subscribe((value) => {
|
||||
this.value.subscribe(() => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this.operator.subscribe((value) => {
|
||||
this.operator.subscribe(() => {
|
||||
this._queryBuilderViewModel.checkIfClauseChanged();
|
||||
});
|
||||
this._groupCheckSubscription = this.checkedForGrouping.subscribe((value) => {
|
||||
this._groupCheckSubscription = this.checkedForGrouping.subscribe(() => {
|
||||
this._queryBuilderViewModel.updateCanGroupClauses();
|
||||
});
|
||||
this.isAndOrFocused = ko.observable<boolean>(false);
|
||||
@@ -280,7 +280,7 @@ export default class QueryClauseViewModel {
|
||||
this._groupCheckSubscription.dispose();
|
||||
}
|
||||
|
||||
this.clauseGroup = null;
|
||||
this._queryBuilderViewModel = null;
|
||||
this.clauseGroup = undefined;
|
||||
this._queryBuilderViewModel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode } from "@fluentui/react";
|
||||
import { DetailsList, DetailsListLayoutMode, IColumn, Pivot, PivotItem, SelectionMode, Text } from "@fluentui/react";
|
||||
import React, { Fragment } from "react";
|
||||
import SplitterLayout from "react-splitter-layout";
|
||||
import "react-splitter-layout/lib/index.css";
|
||||
@@ -120,21 +120,13 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
constructor(props: IQueryTabComponentProps) {
|
||||
super(props);
|
||||
const columns: IColumn[] = [
|
||||
{
|
||||
key: "column1",
|
||||
name: "",
|
||||
minWidth: 16,
|
||||
maxWidth: 16,
|
||||
data: String,
|
||||
fieldName: "toolTip",
|
||||
onRender: this.onRenderColumnItem,
|
||||
},
|
||||
{
|
||||
key: "column2",
|
||||
name: "METRIC",
|
||||
minWidth: 200,
|
||||
data: String,
|
||||
fieldName: "metric",
|
||||
onRender: this.onRenderColumnItem,
|
||||
},
|
||||
{
|
||||
key: "column3",
|
||||
@@ -206,7 +198,12 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
|
||||
public onRenderColumnItem(item: IDocument): JSX.Element {
|
||||
if (item.toolTip !== "") {
|
||||
return <InfoTooltip>{`${item.toolTip}`}</InfoTooltip>;
|
||||
return (
|
||||
<>
|
||||
<InfoTooltip>{`${item.toolTip}`}</InfoTooltip>
|
||||
<Text style={{ paddingLeft: 10, margin: 0 }}>{`${item.metric}`}</Text>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@@ -1002,7 +999,7 @@ export default class QueryTabComponent extends React.Component<IQueryTabComponen
|
||||
"data-order": 2,
|
||||
"data-title": "Query Stats",
|
||||
}}
|
||||
style={{ height: "100%" }}
|
||||
style={{ height: "100%", overflowY: "scroll" }}
|
||||
>
|
||||
{this.state.allResultsMetadata.length > 0 && !this.state.error && (
|
||||
<div className="queryMetricsSummaryContainer">
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
<img class="and-or-svg" src="/And-Or.svg" alt="Group selected clauses" />
|
||||
</button>
|
||||
</th>
|
||||
<th class="clause-table-cell header-background"><!-- Grouping indicator --></th>
|
||||
<th class="clause-table-cell header-background">
|
||||
<!-- Grouping indicator -->
|
||||
</th>
|
||||
<th class="clause-table-cell header-background and-or-header">
|
||||
<span data-bind="text: andLabel"></span>
|
||||
</th>
|
||||
@@ -136,7 +138,7 @@
|
||||
class="select-options-link"
|
||||
data-bind="click: selectQueryOptions, event: { keydown: onselectQueryOptionsKeyDown }"
|
||||
tabindex="0"
|
||||
role="link"
|
||||
role="button"
|
||||
>
|
||||
<span>Choose Columns... </span>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import ko from "knockout";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import loadingIcon from "../../../images/circular_loader_black_16x16.gif";
|
||||
import errorIcon from "../../../images/close-black.svg";
|
||||
import { useObservable } from "../../hooks/useObservable";
|
||||
@@ -32,7 +32,13 @@ export const Tabs = (): JSX.Element => {
|
||||
|
||||
function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
const [hovering, setHovering] = useState(false);
|
||||
const focusTab = useRef<HTMLLIElement>() as MutableRefObject<HTMLLIElement>;
|
||||
|
||||
useEffect(() => {
|
||||
if (active && focusTab.current) {
|
||||
focusTab.current.focus();
|
||||
}
|
||||
});
|
||||
return (
|
||||
<li
|
||||
onMouseOver={() => setHovering(true)}
|
||||
@@ -46,6 +52,7 @@ function TabNav({ tab, active }: { tab: Tab; active: boolean }) {
|
||||
aria-controls={tab.tabId}
|
||||
tabIndex={0}
|
||||
role="tab"
|
||||
ref={focusTab}
|
||||
>
|
||||
<span className="tabNavContentContainer">
|
||||
<a data-toggle="tab" href={"#" + tab.tabId} tabIndex={-1}>
|
||||
|
||||
@@ -291,7 +291,7 @@ export default class UserDefinedFunctionTabContent extends Component<
|
||||
language={"javascript"}
|
||||
content={udfBody}
|
||||
isReadOnly={false}
|
||||
ariaLabel={"Graph JSON"}
|
||||
ariaLabel={"User defined function body"}
|
||||
onContentChanged={this.handleUdfBodyChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ 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 { Areas, Notebook } from "../../Common/Constants";
|
||||
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
@@ -128,15 +128,18 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
notebooksTree.children.push(buildGalleryNotebooksTree());
|
||||
}
|
||||
|
||||
if (myNotebooksContentRoot) {
|
||||
if (
|
||||
myNotebooksContentRoot &&
|
||||
((NotebookUtil.isPhoenixEnabled() &&
|
||||
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected) ||
|
||||
userContext.features.phoenix === false)
|
||||
) {
|
||||
notebooksTree.children.push(buildMyNotebooksTree());
|
||||
}
|
||||
if (container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
// collapse all other notebook nodes
|
||||
notebooksTree.children.forEach((node) => (node.isExpanded = false));
|
||||
notebooksTree.children.push(buildGitHubNotebooksTree(true));
|
||||
} else if (container.notebookManager && !container.notebookManager.gitHubOAuthService.isLoggedIn()) {
|
||||
notebooksTree.children.push(buildGitHubNotebooksTree(false));
|
||||
}
|
||||
}
|
||||
return notebooksTree;
|
||||
@@ -164,7 +167,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
myNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item).then((hasOpened) => {
|
||||
if (hasOpened) {
|
||||
if (
|
||||
hasOpened &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
userContext.features.phoenix === false
|
||||
) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
@@ -183,7 +190,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
gitHubNotebooksContentRoot,
|
||||
(item: NotebookContentItem) => {
|
||||
container.openNotebook(item).then((hasOpened) => {
|
||||
if (hasOpened) {
|
||||
if (
|
||||
hasOpened &&
|
||||
userContext.features.notebooksTemporarilyDown === false &&
|
||||
userContext.features.phoenix === false
|
||||
) {
|
||||
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||
}
|
||||
});
|
||||
@@ -215,23 +226,7 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
||||
},
|
||||
},
|
||||
];
|
||||
const connectGitContextMenu: TreeNodeMenuItem[] = [
|
||||
{
|
||||
label: "Connect to GitHub",
|
||||
onClick: () =>
|
||||
useSidePanel
|
||||
.getState()
|
||||
.openSidePanel(
|
||||
"Connect to GitHub",
|
||||
<GitHubReposPanel
|
||||
explorer={container}
|
||||
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||
junoClientProp={container.notebookManager.junoClient}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
gitHubNotebooksTree.contextMenu = isConnected ? manageGitContextMenu : connectGitContextMenu;
|
||||
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
|
||||
gitHubNotebooksTree.isExpanded = true;
|
||||
gitHubNotebooksTree.isAlphaSorted = true;
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ import UserDefinedFunction from "./UserDefinedFunction";
|
||||
|
||||
export class ResourceTreeAdapter implements ReactAdapter {
|
||||
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||
public static readonly MyNotebooksScratchTitle = "My Notebooks Scratch";
|
||||
public static readonly GitHubReposTitle = "GitHub repos";
|
||||
|
||||
private static readonly DataTitle = "DATA";
|
||||
@@ -132,7 +131,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
type: NotebookContentItemType.File,
|
||||
};
|
||||
this.myNotebooksContentRoot = {
|
||||
name: useNotebook.getState().NotebookFolderName,
|
||||
name: useNotebook.getState().notebookFolderName,
|
||||
path: useNotebook.getState().notebookBasePath,
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
import { Resource, StoredProcedureDefinition } from "@azure/cosmos";
|
||||
import * as ko from "knockout";
|
||||
import * as Constants from "../../Common/Constants";
|
||||
@@ -15,7 +16,7 @@ import { NewStoredProcedureTab } from "../Tabs/StoredProcedureTab/StoredProcedur
|
||||
import TabsBase from "../Tabs/TabsBase";
|
||||
import { useSelectedNode } from "../useSelectedNode";
|
||||
|
||||
const sampleStoredProcedureBody: string = `// SAMPLE STORED PROCEDURE
|
||||
const sampleStoredProcedureBody = `// SAMPLE STORED PROCEDURE
|
||||
function sample(prefix) {
|
||||
var collection = getContext().getCollection();
|
||||
|
||||
@@ -63,7 +64,8 @@ export default class StoredProcedure {
|
||||
this.isExecuteEnabled = userContext.features.executeSproc;
|
||||
}
|
||||
|
||||
public static create(source: ViewModels.Collection, event: MouseEvent) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public static create(source: ViewModels.Collection, _event: MouseEvent): void {
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.StoredProcedures).length + 1;
|
||||
const storedProcedure = <StoredProcedureDefinition>{
|
||||
id: "",
|
||||
@@ -89,7 +91,7 @@ export default class StoredProcedure {
|
||||
useTabs.getState().activateNewTab(storedProcedureTab);
|
||||
}
|
||||
|
||||
public select() {
|
||||
public select(): void {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Stored procedure node",
|
||||
@@ -98,7 +100,7 @@ export default class StoredProcedure {
|
||||
});
|
||||
}
|
||||
|
||||
public open = () => {
|
||||
public open = (): void => {
|
||||
this.select();
|
||||
|
||||
const storedProcedureTabs: NewStoredProcedureTab[] = useTabs
|
||||
@@ -138,7 +140,7 @@ export default class StoredProcedure {
|
||||
useTabs.getState().activateNewTab(storedProcedureTab);
|
||||
}
|
||||
};
|
||||
public delete() {
|
||||
public delete(): void {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
"Are you sure you want to delete the stored procedure?",
|
||||
@@ -149,7 +151,8 @@ export default class StoredProcedure {
|
||||
useTabs.getState().closeTabsByComparator((tab: TabsBase) => tab.node && tab.node.rid === this.rid);
|
||||
this.collection.children.remove(this);
|
||||
},
|
||||
(reason) => {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
"Cancel",
|
||||
|
||||
@@ -22,6 +22,7 @@ export default class Trigger {
|
||||
public triggerType: ko.Observable<string>;
|
||||
public triggerOperation: ko.Observable<string>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(container: Explorer, collection: ViewModels.Collection, data: any) {
|
||||
this.nodeKind = "Trigger";
|
||||
this.container = container;
|
||||
@@ -34,7 +35,7 @@ export default class Trigger {
|
||||
this.triggerType = ko.observable(data.triggerType);
|
||||
}
|
||||
|
||||
public select() {
|
||||
public select(): void {
|
||||
useSelectedNode.getState().setSelectedNode(this);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
description: "Trigger node",
|
||||
@@ -43,7 +44,8 @@ export default class Trigger {
|
||||
});
|
||||
}
|
||||
|
||||
public static create(source: ViewModels.Collection, event: MouseEvent) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public static create(source: ViewModels.Collection, _event: MouseEvent): void {
|
||||
const id = useTabs.getState().getTabs(ViewModels.CollectionTabKind.Triggers).length + 1;
|
||||
const trigger = <StoredProcedureDefinition>{
|
||||
id: "",
|
||||
@@ -99,7 +101,7 @@ export default class Trigger {
|
||||
}
|
||||
};
|
||||
|
||||
public delete() {
|
||||
public delete(): void {
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
"Are you sure you want to delete the trigger?",
|
||||
@@ -110,7 +112,8 @@ export default class Trigger {
|
||||
useTabs.getState().closeTabsByComparator((tab) => tab.node && tab.node.rid === this.rid);
|
||||
this.collection.children.remove(this);
|
||||
},
|
||||
(reason) => {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
"Cancel",
|
||||
|
||||
@@ -64,7 +64,7 @@ export class GitHubOAuthService {
|
||||
return params.state;
|
||||
}
|
||||
|
||||
public async finishOAuth(params: IGitHubConnectorParams) {
|
||||
public async finishOAuth(params: IGitHubConnectorParams): Promise<void> {
|
||||
try {
|
||||
this.validateState(params.state);
|
||||
const response = await this.junoClient.getGitHubToken(params.code);
|
||||
@@ -113,7 +113,7 @@ export class GitHubOAuthService {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public resetToken() {
|
||||
public resetToken(): void {
|
||||
this.token(undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ describe("Pinned repos", () => {
|
||||
});
|
||||
|
||||
it("updatePinnedRepos invokes pinned repos subscribers", async () => {
|
||||
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const callback = jest.fn().mockImplementation(() => {});
|
||||
|
||||
junoClient.subscribeToPinnedRepos(callback);
|
||||
const response = await junoClient.updatePinnedRepos(samplePinnedRepos);
|
||||
@@ -60,7 +61,8 @@ describe("Pinned repos", () => {
|
||||
});
|
||||
|
||||
it("getPinnedRepos invokes pinned repos subscribers", async () => {
|
||||
const callback = jest.fn().mockImplementation((pinnedRepos: IPinnedRepo[]) => {});
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const callback = jest.fn().mockImplementation(() => {});
|
||||
|
||||
junoClient.subscribeToPinnedRepos(callback);
|
||||
const response = await junoClient.getPinnedRepos("scope");
|
||||
@@ -153,7 +155,7 @@ describe("Gallery", () => {
|
||||
it("getSampleNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getSampleNotebooks();
|
||||
@@ -165,7 +167,7 @@ describe("Gallery", () => {
|
||||
it("getPublicNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getPublicNotebooks();
|
||||
@@ -178,7 +180,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getNotebookInfo(id);
|
||||
@@ -191,7 +193,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
text: () => undefined as any,
|
||||
text: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getNotebookContent(id);
|
||||
@@ -204,7 +206,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
const response = await junoClient.increaseNotebookViews(id);
|
||||
|
||||
@@ -218,7 +220,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.increaseNotebookDownloadCount(id);
|
||||
@@ -243,7 +245,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.favoriteNotebook(id);
|
||||
@@ -268,7 +270,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.unfavoriteNotebook(id);
|
||||
@@ -292,7 +294,7 @@ describe("Gallery", () => {
|
||||
it("getFavoriteNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getFavoriteNotebooks();
|
||||
@@ -315,7 +317,7 @@ describe("Gallery", () => {
|
||||
it("getPublishedNotebooks", async () => {
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.getPublishedNotebooks();
|
||||
@@ -339,7 +341,7 @@ describe("Gallery", () => {
|
||||
const id = "id";
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.deleteNotebook(id);
|
||||
@@ -369,7 +371,7 @@ describe("Gallery", () => {
|
||||
const addLinkToNotebookViewer = true;
|
||||
window.fetch = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
json: () => undefined as any,
|
||||
json: () => undefined as undefined,
|
||||
});
|
||||
|
||||
const response = await junoClient.publishNotebook(name, description, tags, thumbnailUrl, content);
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface IPublishNotebookRequest {
|
||||
description: string;
|
||||
tags: string[];
|
||||
thumbnailUrl: string;
|
||||
content: any;
|
||||
content: unknown;
|
||||
addLinkToNotebookViewer: boolean;
|
||||
}
|
||||
|
||||
|
||||
57
src/Localization/en/GraphAPICompute.json
Normal file
57
src/Localization/en/GraphAPICompute.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"GraphAPIDescription": "Provision a Graph API Compute for your Azure Cosmos DB account.",
|
||||
"GraphAPICompute": "Graph GraphAPI Compute",
|
||||
"Provisioned": "Provisioned",
|
||||
"Deprovisioned": "Deprovisioned",
|
||||
"Compute": "Compute",
|
||||
"GremlinV2": "GremlinV2",
|
||||
"LearnAboutCompute": "Learn more about GraphAPI Compute.",
|
||||
"DeprovisioningDetailsText": "Learn more about deprovisioning the GraphAPI Compute.",
|
||||
"ComputePricing": "Learn more about GraphAPI Compute 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": "Graph GraphAPI Compute resource is being created.",
|
||||
"CreateInitializeTitle": "Provisioning resource",
|
||||
"CreateInitializeMessage": "GraphAPI Compute resource will be provisioned.",
|
||||
"CreateSuccessTitle": "Resource provisioned",
|
||||
"CreateSuccesseMessage": "GraphAPI Compute resource provisioned.",
|
||||
"CreateFailureTitle": "Failed to provision resource",
|
||||
"CreateFailureMessage": "GraphAPI Compute resource provisioning failed.",
|
||||
"UpdateMessage": "GraphAPI Compute resource is being updated.",
|
||||
"UpdateInitializeTitle": "Updating resource",
|
||||
"UpdateInitializeMessage": "GraphAPI Compute resource will be updated.",
|
||||
"UpdateSuccessTitle": "Resource updated",
|
||||
"UpdateSuccesseMessage": "GraphAPI Compute resource updated.",
|
||||
"UpdateFailureTitle": "Failed to update resource",
|
||||
"UpdateFailureMessage": "GraphAPI Compute resource updation failed.",
|
||||
"DeleteMessage": "GraphAPI Compute resource is being deleted.",
|
||||
"DeleteInitializeTitle": "Deleting resource",
|
||||
"DeleteInitializeMessage": "GraphAPI Compute resource will be deleted.",
|
||||
"DeleteSuccessTitle": "Resource deleted",
|
||||
"DeleteSuccesseMessage": "GraphAPI Compute resource deleted.",
|
||||
"DeleteFailureTitle": "Failed to delete resource",
|
||||
"DeleteFailureMessage": "GraphAPI Compute resource deletion failed.",
|
||||
"CannotSave": "Cannot save the changes to the GraphAPI Compute resource at the moment.",
|
||||
"GraphAccountEndpoint": "Graph Account Endpoint",
|
||||
"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",
|
||||
"ApproximateCost": "Approximate Cost Per Hour",
|
||||
"CostText": "Hourly cost of the GraphAPI Compute resource depends on the SKU selection, number of instances per region, and number of regions.",
|
||||
"ConnectionString": "Connection String",
|
||||
"ConnectionStringText": "To use the GraphAPI Compute, use the connection string shown in ",
|
||||
"KeysBlade": "the keys blade.",
|
||||
"MetricsString": "Metrics",
|
||||
"MetricsText": "Monitor the CPU and memory usage for the GraphAPI Compute instances in ",
|
||||
"MetricsBlade": "the metrics blade.",
|
||||
"MonitorUsage": "Monitor Usage",
|
||||
"ResizingDecisionText": "Number of instances has to be 1 during provisioning. Instances can only be incremented by 1 at once. ",
|
||||
"ResizingDecisionLink": "Learn more about GraphAPI Compute sizing.",
|
||||
"WarningBannerOnUpdate": "Adding or modifying GraphAPI Compute instances may affect your bill.",
|
||||
"WarningBannerOnDelete": "After deprovisioning the GraphAPI Compute, you will not be able to connect to the Graph API account."
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ConnectionStatusType, HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import * as DataModels from "../Contracts/DataModels";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { userContext } from "../UserContext";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
|
||||
@@ -15,7 +13,6 @@ export interface IPhoenixConnectionInfoResult {
|
||||
}
|
||||
export interface IProvosionData {
|
||||
cosmosEndpoint: string;
|
||||
resourceId: string;
|
||||
dbAccountName: string;
|
||||
aadToken: string;
|
||||
resourceGroup: string;
|
||||
@@ -25,29 +22,29 @@ export class PhoenixClient {
|
||||
public async containerConnectionInfo(
|
||||
provisionData: IProvosionData
|
||||
): Promise<IPhoenixResponse<IPhoenixConnectionInfoResult>> {
|
||||
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/provision`, {
|
||||
method: "POST",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
let data: IPhoenixConnectionInfoResult;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
} else {
|
||||
const connectionStatus: DataModels.ContainerConnectionInfo = {
|
||||
status: ConnectionStatusType.Failed,
|
||||
try {
|
||||
const response = await window.fetch(`${this.getPhoenixContainerPoolingEndPoint()}/allocate`, {
|
||||
method: "POST",
|
||||
headers: PhoenixClient.getHeaders(),
|
||||
body: JSON.stringify(provisionData),
|
||||
});
|
||||
let data: IPhoenixConnectionInfoResult;
|
||||
if (response.status === HttpStatusCodes.OK) {
|
||||
data = await response.json();
|
||||
}
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
public static getPhoenixEndpoint(): string {
|
||||
const phoenixEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
const phoenixEndpoint =
|
||||
userContext.features.phoenixEndpoint ?? userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
|
||||
if (configContext.allowedJunoOrigins.indexOf(new URL(phoenixEndpoint).origin) === -1) {
|
||||
const error = `${phoenixEndpoint} not allowed as juno endpoint`;
|
||||
console.error(error);
|
||||
@@ -58,7 +55,7 @@ export class PhoenixClient {
|
||||
}
|
||||
|
||||
public getPhoenixContainerPoolingEndPoint(): string {
|
||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/containerpooling`;
|
||||
return `${PhoenixClient.getPhoenixEndpoint()}/api/controlplane/toolscontainer`;
|
||||
}
|
||||
private static getHeaders(): HeadersInit {
|
||||
const authorizationHeader = getAuthorizationHeader();
|
||||
|
||||
@@ -4,28 +4,27 @@ import * as DataModels from "../../Contracts/DataModels";
|
||||
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AuthHeadersUtil {
|
||||
public static async generateEncryptedToken(readOnly: boolean = false): Promise<DataModels.GenerateTokenResponse> {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
|
||||
export const generateEncryptedToken = async (readOnly = false): Promise<DataModels.GenerateTokenResponse> => {
|
||||
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + _generateResourceUrl();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const headers: any = { authorization: userContext.authorizationToken };
|
||||
headers[Constants.HttpHeaders.getReadOnlyKey] = readOnly;
|
||||
|
||||
const response = await fetch(url, { method: "POST", headers });
|
||||
const result = await response.json();
|
||||
// This API has a quirk where the response must be parsed to JSON twice
|
||||
return JSON.parse(result);
|
||||
}
|
||||
const response = await fetch(url, { method: "POST", headers });
|
||||
const result = await response.json();
|
||||
// This API has a quirk where the response must be parsed to JSON twice
|
||||
return JSON.parse(result);
|
||||
};
|
||||
|
||||
private static _generateResourceUrl(): string {
|
||||
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
|
||||
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
|
||||
const sid = subscriptionId || "";
|
||||
const rg = resourceGroup || "";
|
||||
const dba = databaseAccount?.name || "";
|
||||
const resourceUrl = encodeURIComponent(accountEndpoint);
|
||||
const rid = "";
|
||||
const rtype = "";
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
}
|
||||
}
|
||||
export const _generateResourceUrl = (): string => {
|
||||
const { databaseAccount, resourceGroup, subscriptionId } = userContext;
|
||||
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(userContext.apiType);
|
||||
const accountEndpoint = databaseAccount?.properties?.documentEndpoint || "";
|
||||
const sid = subscriptionId || "";
|
||||
const rg = resourceGroup || "";
|
||||
const dba = databaseAccount?.name || "";
|
||||
const resourceUrl = encodeURIComponent(accountEndpoint);
|
||||
const rid = "";
|
||||
const rtype = "";
|
||||
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { extractFeatures } from "./extractFeatures";
|
||||
import { extractFeatures, hasFlag } from "./extractFeatures";
|
||||
|
||||
describe("extractFeatures", () => {
|
||||
it("correctly detects feature flags in a case insensitive manner", () => {
|
||||
@@ -14,9 +14,25 @@ describe("extractFeatures", () => {
|
||||
});
|
||||
|
||||
const features = extractFeatures(params);
|
||||
|
||||
expect(features.notebookServerUrl).toBe(url);
|
||||
expect(features.notebookServerToken).toBe(token);
|
||||
expect(features.enableNotebooks).toBe(notebooksEnabled);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasFlag", () => {
|
||||
it("correctly determines if value has flag", () => {
|
||||
const desiredFlag = "readDocument";
|
||||
|
||||
const singleFlagValue = "readDocument";
|
||||
const multipleFlagValues = "readDocument|createDocument";
|
||||
const differentFlagValue = "createDocument";
|
||||
|
||||
expect(hasFlag(singleFlagValue, desiredFlag)).toBe(true);
|
||||
expect(hasFlag(multipleFlagValues, desiredFlag)).toBe(true);
|
||||
expect(hasFlag(differentFlagValue, desiredFlag)).toBe(false);
|
||||
expect(hasFlag(multipleFlagValues, (undefined as unknown) as string)).toBe(false);
|
||||
expect(hasFlag((undefined as unknown) as string, desiredFlag)).toBe(false);
|
||||
expect(hasFlag((undefined as unknown) as string, (undefined as unknown) as string)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ export type Features = {
|
||||
readonly enableKoResourceTree: boolean;
|
||||
readonly hostedDataExplorer: boolean;
|
||||
readonly junoEndpoint?: string;
|
||||
readonly phoenixEndpoint?: string;
|
||||
readonly livyEndpoint?: string;
|
||||
readonly notebookBasePath?: string;
|
||||
readonly notebookServerToken?: string;
|
||||
@@ -29,6 +30,8 @@ export type Features = {
|
||||
readonly pr?: string;
|
||||
readonly showMinRUSurvey: boolean;
|
||||
readonly ttl90Days: boolean;
|
||||
readonly mongoProxyEndpoint?: string;
|
||||
readonly mongoProxyAPIs?: string;
|
||||
readonly notebooksTemporarilyDown: boolean;
|
||||
};
|
||||
|
||||
@@ -63,7 +66,10 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
enableKoResourceTree: "true" === get("enablekoresourcetree"),
|
||||
executeSproc: "true" === get("dataexplorerexecutesproc"),
|
||||
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
|
||||
mongoProxyEndpoint: get("mongoproxyendpoint"),
|
||||
mongoProxyAPIs: get("mongoproxyapis"),
|
||||
junoEndpoint: get("junoendpoint"),
|
||||
phoenixEndpoint: get("phoenixendpoint"),
|
||||
livyEndpoint: get("livyendpoint"),
|
||||
notebookBasePath: get("notebookbasepath"),
|
||||
notebookServerToken: get("notebookservertoken"),
|
||||
@@ -80,3 +86,12 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
|
||||
phoenix: "true" === get("phoenix"),
|
||||
};
|
||||
}
|
||||
|
||||
export function hasFlag(flags: string | undefined, desiredFlag: string | undefined): boolean {
|
||||
if (!flags || !desiredFlag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const features = flags.split("|");
|
||||
return features.find((feature) => feature === desiredFlag) ? true : false;
|
||||
}
|
||||
|
||||
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
200
src/SelfServe/GraphAPICompute/GraphAPICompute.rp.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { configContext } from "../../ConfigContext";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||
import { RefreshResult } from "../SelfServeTypes";
|
||||
import GraphAPICompute from "./GraphAPICompute";
|
||||
import {
|
||||
FetchPricesResponse,
|
||||
RegionsResponse,
|
||||
GraphAPIComputeServiceResource,
|
||||
UpdateComputeRequestParameters,
|
||||
} from "./GraphAPICompute.types";
|
||||
|
||||
const apiVersion = "2021-04-01-preview";
|
||||
const gremlinV2 = "GremlinV2";
|
||||
|
||||
export enum ResourceStatus {
|
||||
Running = "Running",
|
||||
Creating = "Creating",
|
||||
Updating = "Updating",
|
||||
Deleting = "Deleting",
|
||||
}
|
||||
|
||||
export interface ComputeResponse {
|
||||
sku: string;
|
||||
instances: number;
|
||||
status: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export const getPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}/services/${gremlinV2}`;
|
||||
};
|
||||
|
||||
export const updateComputeResource = async (sku: string, instances: number): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const body: UpdateComputeRequestParameters = {
|
||||
properties: {
|
||||
instanceSize: sku,
|
||||
instanceCount: instances,
|
||||
serviceType: gremlinV2,
|
||||
},
|
||||
};
|
||||
const telemetryData = { ...body, httpMethod: "PUT", selfServeClassName: GraphAPICompute.name };
|
||||
const updateTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "PUT",
|
||||
apiVersion,
|
||||
body,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, updateTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { ...body, e, selfServeClassName: GraphAPICompute.name };
|
||||
selfServeTraceFailure(failureTelemetry, updateTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const deleteComputeResource = async (): Promise<string> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "DELETE", selfServeClassName: GraphAPICompute.name };
|
||||
const deleteTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "DELETE",
|
||||
apiVersion,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, deleteTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name };
|
||||
selfServeTraceFailure(failureTelemetry, deleteTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.operationStatusUrl;
|
||||
};
|
||||
|
||||
export const getComputeResource = async (): Promise<GraphAPIComputeServiceResource> => {
|
||||
const path = getPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name);
|
||||
const telemetryData = { httpMethod: "GET", selfServeClassName: GraphAPICompute.name };
|
||||
const getResourceTimeStamp = selfServeTraceStart(telemetryData);
|
||||
let armRequestResult;
|
||||
try {
|
||||
armRequestResult = await armRequestWithoutPolling<GraphAPIComputeServiceResource>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path,
|
||||
method: "GET",
|
||||
apiVersion,
|
||||
});
|
||||
selfServeTraceSuccess(telemetryData, getResourceTimeStamp);
|
||||
} catch (e) {
|
||||
const failureTelemetry = { e, selfServeClassName: GraphAPICompute.name };
|
||||
selfServeTraceFailure(failureTelemetry, getResourceTimeStamp);
|
||||
throw e;
|
||||
}
|
||||
return armRequestResult?.result;
|
||||
};
|
||||
|
||||
export const getCurrentProvisioningState = async (): Promise<ComputeResponse> => {
|
||||
try {
|
||||
const response = await getComputeResource();
|
||||
return {
|
||||
sku: response.properties.instanceSize,
|
||||
instances: response.properties.instanceCount,
|
||||
status: response.properties.status,
|
||||
endpoint: response.properties.GraphAPIComputeEndPoint,
|
||||
};
|
||||
} catch (e) {
|
||||
return { sku: undefined, instances: undefined, status: undefined, endpoint: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
export const refreshComputeProvisioning = async (): Promise<RefreshResult> => {
|
||||
try {
|
||||
const response = await getComputeResource();
|
||||
if (response.properties.status === ResourceStatus.Running.toString()) {
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
} else if (response.properties.status === ResourceStatus.Creating.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "CreateMessage" };
|
||||
} else if (response.properties.status === ResourceStatus.Deleting.toString()) {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "DeleteMessage" };
|
||||
} else {
|
||||
return { isUpdateInProgress: true, updateInProgressMessageTKey: "UpdateMessage" };
|
||||
}
|
||||
} catch {
|
||||
//TODO differentiate between different failures
|
||||
return { isUpdateInProgress: false, updateInProgressMessageTKey: undefined };
|
||||
}
|
||||
};
|
||||
|
||||
const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getReadRegions = async (): Promise<Array<string>> => {
|
||||
try {
|
||||
const readRegions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getGeneralPath(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name),
|
||||
method: "GET",
|
||||
apiVersion: "2021-04-01-preview",
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
readRegions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
readRegions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
return readRegions;
|
||||
} catch (err) {
|
||||
return new Array<string>();
|
||||
}
|
||||
};
|
||||
|
||||
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||
};
|
||||
|
||||
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
|
||||
for (const region of regions) {
|
||||
const regionPriceMap = new Map<string, number>();
|
||||
|
||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
||||
method: "POST",
|
||||
apiVersion: "2020-01-01-preview",
|
||||
queryParams: {
|
||||
filter:
|
||||
"armRegionName eq '" +
|
||||
region +
|
||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
||||
},
|
||||
});
|
||||
|
||||
for (const item of response.result.Items) {
|
||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
||||
}
|
||||
priceMap.set(region, regionPriceMap);
|
||||
}
|
||||
|
||||
return priceMap;
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
423
src/SelfServe/GraphAPICompute/GraphAPICompute.tsx
Normal file
@@ -0,0 +1,423 @@
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import { selfServeTrace } from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
DescriptionType,
|
||||
Info,
|
||||
InputType,
|
||||
NumberUiType,
|
||||
OnSaveResult,
|
||||
RefreshResult,
|
||||
SelfServeBaseClass,
|
||||
SmartUiInput,
|
||||
} from "../SelfServeTypes";
|
||||
import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
||||
import {
|
||||
deleteComputeResource,
|
||||
getCurrentProvisioningState,
|
||||
getPriceMap,
|
||||
getReadRegions,
|
||||
refreshComputeProvisioning,
|
||||
updateComputeResource,
|
||||
} from "./GraphAPICompute.rp";
|
||||
|
||||
const costPerHourDefaultValue: Description = {
|
||||
textTKey: "CostText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing",
|
||||
textTKey: "ComputePricing",
|
||||
},
|
||||
};
|
||||
|
||||
const connectionStringValue: Description = {
|
||||
textTKey: "ConnectionStringText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.SqlKeys),
|
||||
textTKey: "KeysBlade",
|
||||
},
|
||||
};
|
||||
|
||||
const metricsStringValue: Description = {
|
||||
textTKey: "MetricsText",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: generateBladeLink(BladeType.Metrics),
|
||||
textTKey: "MetricsBlade",
|
||||
},
|
||||
};
|
||||
|
||||
const CosmosD4s = "Cosmos.D4s";
|
||||
const CosmosD8s = "Cosmos.D8s";
|
||||
const CosmosD16s = "Cosmos.D16s";
|
||||
|
||||
const onSKUChange = (newValue: InputType, currentValues: Map<string, SmartUiInput>): Map<string, SmartUiInput> => {
|
||||
currentValues.set("sku", { value: newValue });
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(newValue as string, currentValues.get("instances").value as number),
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const onNumberOfInstancesChange = (
|
||||
newValue: InputType,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentValues.set("instances", { value: newValue });
|
||||
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
|
||||
const baselineInstances = baselineValues.get("instances")?.value as number;
|
||||
if (!ComputeOriginallyEnabled || baselineInstances !== newValue) {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview",
|
||||
textTKey: "ComputePricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
} else {
|
||||
currentValues.set("warningBanner", undefined);
|
||||
}
|
||||
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(currentValues.get("sku").value as string, newValue as number),
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const onEnableComputeChange = (
|
||||
newValue: InputType,
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: ReadonlyMap<string, SmartUiInput>
|
||||
): Map<string, SmartUiInput> => {
|
||||
currentValues.set("enableCompute", { value: newValue });
|
||||
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
|
||||
if (ComputeOriginallyEnabled === newValue) {
|
||||
currentValues.set("sku", baselineValues.get("sku"));
|
||||
currentValues.set("instances", baselineValues.get("instances"));
|
||||
currentValues.set("costPerHour", baselineValues.get("costPerHour"));
|
||||
currentValues.set("warningBanner", baselineValues.get("warningBanner"));
|
||||
currentValues.set("connectionString", baselineValues.get("connectionString"));
|
||||
currentValues.set("metricsString", baselineValues.get("metricsString"));
|
||||
return currentValues;
|
||||
}
|
||||
|
||||
currentValues.set("warningBanner", undefined);
|
||||
if (newValue === true) {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnUpdate",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //needs updating
|
||||
textTKey: "ComputePricing",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
currentValues.set("costPerHour", {
|
||||
value: calculateCost(baselineValues.get("sku").value as string, baselineValues.get("instances").value as number),
|
||||
hidden: false,
|
||||
});
|
||||
} else {
|
||||
currentValues.set("warningBanner", {
|
||||
value: {
|
||||
textTKey: "WarningBannerOnDelete",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", // needs updating
|
||||
textTKey: "DeprovisioningDetailsText",
|
||||
},
|
||||
} as Description,
|
||||
hidden: false,
|
||||
});
|
||||
|
||||
currentValues.set("costPerHour", { value: costPerHourDefaultValue, hidden: true });
|
||||
}
|
||||
const sku = currentValues.get("sku");
|
||||
const hideAttributes = newValue === undefined || !(newValue as boolean);
|
||||
currentValues.set("sku", {
|
||||
value: sku.value,
|
||||
hidden: hideAttributes,
|
||||
disabled: ComputeOriginallyEnabled,
|
||||
});
|
||||
currentValues.set("instances", {
|
||||
value: 1,
|
||||
hidden: hideAttributes,
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
currentValues.set("connectionString", {
|
||||
value: connectionStringValue,
|
||||
hidden: !newValue || !ComputeOriginallyEnabled,
|
||||
});
|
||||
|
||||
currentValues.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: !newValue || !ComputeOriginallyEnabled,
|
||||
});
|
||||
|
||||
return currentValues;
|
||||
};
|
||||
|
||||
const skuDropDownItems: ChoiceItem[] = [
|
||||
{ labelTKey: "CosmosD4s", key: CosmosD4s },
|
||||
{ labelTKey: "CosmosD8s", key: CosmosD8s },
|
||||
{ labelTKey: "CosmosD16s", key: CosmosD16s },
|
||||
];
|
||||
|
||||
const getSkus = async (): Promise<ChoiceItem[]> => {
|
||||
return skuDropDownItems;
|
||||
};
|
||||
|
||||
const NumberOfInstancesDropdownInfo: Info = {
|
||||
messageTKey: "ResizingDecisionText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-size", // todo
|
||||
textTKey: "ResizingDecisionLink",
|
||||
},
|
||||
};
|
||||
|
||||
const getInstancesMin = async (): Promise<number> => {
|
||||
return 1;
|
||||
};
|
||||
|
||||
const getInstancesMax = async (): Promise<number> => {
|
||||
return 5;
|
||||
};
|
||||
|
||||
const ApproximateCostDropDownInfo: Info = {
|
||||
messageTKey: "CostText",
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-pricing", //todo
|
||||
textTKey: "ComputePricing",
|
||||
},
|
||||
};
|
||||
|
||||
let priceMap: Map<string, Map<string, number>>;
|
||||
let regions: Array<string>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
const incrementalCost = priceMap.get(region).get(skuName.replace("Cosmos.", ""));
|
||||
if (incrementalCost === undefined) {
|
||||
throw new Error("Value not found in map.");
|
||||
}
|
||||
costPerHour += incrementalCost;
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
return {
|
||||
textTKey: `${costPerHour} USD`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
return costPerHourDefaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
@IsDisplayable()
|
||||
@RefreshOptions({ retryIntervalInMs: 20000 })
|
||||
export default class GraphAPICompute extends SelfServeBaseClass {
|
||||
public onRefresh = async (): Promise<RefreshResult> => {
|
||||
return await refreshComputeProvisioning();
|
||||
};
|
||||
|
||||
public onSave = async (
|
||||
currentValues: Map<string, SmartUiInput>,
|
||||
baselineValues: Map<string, SmartUiInput>
|
||||
): Promise<OnSaveResult> => {
|
||||
selfServeTrace({ selfServeClassName: GraphAPICompute.name });
|
||||
|
||||
const ComputeCurrentlyEnabled = currentValues.get("enableCompute")?.value as boolean;
|
||||
const ComputeOriginallyEnabled = baselineValues.get("enableCompute")?.value as boolean;
|
||||
|
||||
currentValues.set("warningBanner", undefined);
|
||||
|
||||
if (ComputeOriginallyEnabled) {
|
||||
if (!ComputeCurrentlyEnabled) {
|
||||
const operationStatusUrl = await deleteComputeResource();
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "DeleteInitializeTitle",
|
||||
messageTKey: "DeleteInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "DeleteSuccessTitle",
|
||||
messageTKey: "DeleteSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "DeleteFailureTitle",
|
||||
messageTKey: "DeleteFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const sku = currentValues.get("sku")?.value as string;
|
||||
const instances = currentValues.get("instances").value as number;
|
||||
const operationStatusUrl = await updateComputeResource(sku, instances);
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "UpdateInitializeTitle",
|
||||
messageTKey: "UpdateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "UpdateSuccessTitle",
|
||||
messageTKey: "UpdateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "UpdateFailureTitle",
|
||||
messageTKey: "UpdateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
const sku = currentValues.get("sku")?.value as string;
|
||||
const instances = currentValues.get("instances").value as number;
|
||||
const operationStatusUrl = await updateComputeResource(sku, instances);
|
||||
return {
|
||||
operationStatusUrl: operationStatusUrl,
|
||||
portalNotification: {
|
||||
initialize: {
|
||||
titleTKey: "CreateInitializeTitle",
|
||||
messageTKey: "CreateInitializeMessage",
|
||||
},
|
||||
success: {
|
||||
titleTKey: "CreateSuccessTitle",
|
||||
messageTKey: "CreateSuccesseMessage",
|
||||
},
|
||||
failure: {
|
||||
titleTKey: "CreateFailureTitle",
|
||||
messageTKey: "CreateFailureMessage",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
public initialize = async (): Promise<Map<string, SmartUiInput>> => {
|
||||
// Based on the RP call enableCompute will be true if it has not yet been enabled and false if it has.
|
||||
const defaults = new Map<string, SmartUiInput>();
|
||||
defaults.set("enableCompute", { value: false });
|
||||
defaults.set("sku", { value: CosmosD4s, hidden: true });
|
||||
defaults.set("instances", { value: 1, hidden: true });
|
||||
defaults.set("costPerHour", undefined);
|
||||
defaults.set("connectionString", undefined);
|
||||
defaults.set("metricsString", {
|
||||
value: undefined,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
regions = await getReadRegions();
|
||||
priceMap = await getPriceMap(regions);
|
||||
const response = await getCurrentProvisioningState();
|
||||
if (response.status && response.status === "Creating") {
|
||||
defaults.set("enableCompute", { value: true });
|
||||
defaults.set("sku", { value: response.sku, disabled: true });
|
||||
defaults.set("instances", { value: response.instances, disabled: true });
|
||||
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||
defaults.set("connectionString", {
|
||||
value: connectionStringValue,
|
||||
hidden: true,
|
||||
});
|
||||
defaults.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: true,
|
||||
});
|
||||
} else if (response.status && response.status !== "Deleting") {
|
||||
defaults.set("enableCompute", { value: true });
|
||||
defaults.set("sku", { value: response.sku, disabled: true });
|
||||
defaults.set("instances", { value: response.instances });
|
||||
defaults.set("costPerHour", { value: calculateCost(response.sku, response.instances) });
|
||||
defaults.set("connectionString", {
|
||||
value: connectionStringValue,
|
||||
hidden: false,
|
||||
});
|
||||
defaults.set("metricsString", {
|
||||
value: metricsStringValue,
|
||||
hidden: false,
|
||||
});
|
||||
}
|
||||
|
||||
defaults.set("warningBanner", undefined);
|
||||
return defaults;
|
||||
};
|
||||
|
||||
@Values({
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
warningBanner: string;
|
||||
|
||||
@Values({
|
||||
description: {
|
||||
textTKey: "GraphAPIDescription",
|
||||
type: DescriptionType.Text,
|
||||
link: {
|
||||
href: "https://aka.ms/cosmos-db-dedicated-gateway-overview", //todo
|
||||
textTKey: "LearnAboutCompute",
|
||||
},
|
||||
},
|
||||
})
|
||||
description: string;
|
||||
|
||||
@OnChange(onEnableComputeChange)
|
||||
@Values({
|
||||
labelTKey: "Compute",
|
||||
trueLabelTKey: "Provisioned",
|
||||
falseLabelTKey: "Deprovisioned",
|
||||
})
|
||||
enableCompute: boolean;
|
||||
|
||||
@OnChange(onSKUChange)
|
||||
@Values({
|
||||
labelTKey: "SKUs",
|
||||
choices: getSkus,
|
||||
placeholderTKey: "SKUsPlaceHolder",
|
||||
})
|
||||
sku: ChoiceItem;
|
||||
|
||||
@OnChange(onNumberOfInstancesChange)
|
||||
@PropertyInfo(NumberOfInstancesDropdownInfo)
|
||||
@Values({
|
||||
labelTKey: "NumberOfInstances",
|
||||
min: getInstancesMin,
|
||||
max: getInstancesMax,
|
||||
step: 1,
|
||||
uiType: NumberUiType.Spinner,
|
||||
})
|
||||
instances: number;
|
||||
|
||||
@PropertyInfo(ApproximateCostDropDownInfo)
|
||||
@Values({
|
||||
labelTKey: "ApproximateCost",
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
costPerHour: string;
|
||||
|
||||
@Values({
|
||||
labelTKey: "ConnectionString",
|
||||
isDynamicDescription: true,
|
||||
})
|
||||
connectionString: string;
|
||||
|
||||
@Values({
|
||||
labelTKey: "MonitorUsage",
|
||||
description: metricsStringValue,
|
||||
})
|
||||
metricsString: string;
|
||||
}
|
||||
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
65
src/SelfServe/GraphAPICompute/GraphAPICompute.types.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export enum Regions {
|
||||
NorthCentralUS = "NorthCentralUS",
|
||||
WestUS = "WestUS",
|
||||
EastUS2 = "EastUS2",
|
||||
}
|
||||
|
||||
export interface AccountProps {
|
||||
regions: Regions;
|
||||
enableLogging: boolean;
|
||||
accountName: string;
|
||||
collectionThroughput: number;
|
||||
dbThroughput: number;
|
||||
}
|
||||
|
||||
export type GraphAPIComputeServiceResource = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
properties: GraphAPIComputeServiceProps;
|
||||
locations: GraphAPIComputeServiceLocations;
|
||||
};
|
||||
export type GraphAPIComputeServiceProps = {
|
||||
serviceType: string;
|
||||
creationTime: string;
|
||||
status: string;
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
GraphAPIComputeEndPoint: string;
|
||||
};
|
||||
|
||||
export type GraphAPIComputeServiceLocations = {
|
||||
location: string;
|
||||
status: string;
|
||||
GraphAPIComputeEndpoint: string;
|
||||
};
|
||||
|
||||
export type UpdateComputeRequestParameters = {
|
||||
properties: UpdateComputeRequestProperties;
|
||||
};
|
||||
|
||||
export type UpdateComputeRequestProperties = {
|
||||
instanceSize: string;
|
||||
instanceCount: number;
|
||||
serviceType: string;
|
||||
};
|
||||
|
||||
export type FetchPricesResponse = {
|
||||
Items: Array<PriceItem>;
|
||||
NextPageLink: string | undefined;
|
||||
Count: number;
|
||||
};
|
||||
|
||||
export type PriceItem = {
|
||||
retailPrice: number;
|
||||
skuName: string;
|
||||
};
|
||||
|
||||
export type RegionsResponse = {
|
||||
locations: Array<RegionItem>;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type RegionItem = {
|
||||
locationName: string;
|
||||
};
|
||||
@@ -50,6 +50,14 @@ const getDescriptor = async (selfServeType: SelfServeType): Promise<SelfServeDes
|
||||
await loadTranslations(sqlX.constructor.name);
|
||||
return sqlX.toSelfServeDescriptor();
|
||||
}
|
||||
case SelfServeType.graphapicompute: {
|
||||
const GraphAPICompute = await import(
|
||||
/* webpackChunkName: "GraphAPICompute" */ "./GraphAPICompute/GraphAPICompute"
|
||||
);
|
||||
const graphAPICompute = new GraphAPICompute.default();
|
||||
await loadTranslations(graphAPICompute.constructor.name);
|
||||
return graphAPICompute.toSelfServeDescriptor();
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ export enum SelfServeType {
|
||||
// Add your self serve types here
|
||||
example = "example",
|
||||
sqlx = "sqlx",
|
||||
graphapicompute = "graphapicompute",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -138,9 +138,17 @@ const getGeneralPath = (subscriptionId: string, resourceGroup: string, name: str
|
||||
return `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.DocumentDB/databaseAccounts/${name}`;
|
||||
};
|
||||
|
||||
export const getReadRegions = async (): Promise<Array<string>> => {
|
||||
export const getRegions = async (): Promise<Array<string>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getRegions",
|
||||
description: "",
|
||||
selfServeClassName: SqlX.name,
|
||||
};
|
||||
const getRegionsTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const readRegions = new Array<string>();
|
||||
const regions = new Array<string>();
|
||||
|
||||
const response = await armRequestWithoutPolling<RegionsResponse>({
|
||||
host: configContext.ARM_ENDPOINT,
|
||||
@@ -150,14 +158,18 @@ export const getReadRegions = async (): Promise<Array<string>> => {
|
||||
});
|
||||
|
||||
if (response.result.location !== undefined) {
|
||||
readRegions.push(response.result.location.replace(" ", "").toLowerCase());
|
||||
regions.push(response.result.location.split(" ").join("").toLowerCase());
|
||||
} else {
|
||||
for (const location of response.result.locations) {
|
||||
readRegions.push(location.locationName.replace(" ", "").toLowerCase());
|
||||
regions.push(location.locationName.split(" ").join("").toLowerCase());
|
||||
}
|
||||
}
|
||||
return readRegions;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getRegionsTimestamp);
|
||||
return regions;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getRegionsTimestamp);
|
||||
return new Array<string>();
|
||||
}
|
||||
};
|
||||
@@ -167,6 +179,14 @@ const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||
};
|
||||
|
||||
export const getPriceMap = async (regions: Array<string>): Promise<Map<string, Map<string, number>>> => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "getPriceMap",
|
||||
description: "fetch prices API call",
|
||||
selfServeClassName: SqlX.name,
|
||||
};
|
||||
const getPriceMapTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
const priceMap = new Map<string, Map<string, number>>();
|
||||
|
||||
@@ -192,8 +212,12 @@ export const getPriceMap = async (regions: Array<string>): Promise<Map<string, M
|
||||
priceMap.set(region, regionPriceMap);
|
||||
}
|
||||
|
||||
selfServeTraceSuccess(telemetryData, getPriceMapTimestamp);
|
||||
return priceMap;
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, getPriceMapTimestamp);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { IsDisplayable, OnChange, PropertyInfo, RefreshOptions, Values } from "../Decorators";
|
||||
import { selfServeTrace } from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
selfServeTrace,
|
||||
selfServeTraceFailure,
|
||||
selfServeTraceStart,
|
||||
selfServeTraceSuccess,
|
||||
} from "../SelfServeTelemetryProcessor";
|
||||
import {
|
||||
ChoiceItem,
|
||||
Description,
|
||||
@@ -17,7 +22,7 @@ import {
|
||||
deleteDedicatedGatewayResource,
|
||||
getCurrentProvisioningState,
|
||||
getPriceMap,
|
||||
getReadRegions,
|
||||
getRegions,
|
||||
refreshDedicatedGatewayProvisioning,
|
||||
updateDedicatedGatewayResource,
|
||||
} from "./SqlX.rp";
|
||||
@@ -205,6 +210,14 @@ let priceMap: Map<string, Map<string, number>>;
|
||||
let regions: Array<string>;
|
||||
|
||||
const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
const telemetryData = {
|
||||
feature: "Calculate approximate cost",
|
||||
function: "calculateCost",
|
||||
description: "performs final calculation",
|
||||
selfServeClassName: SqlX.name,
|
||||
};
|
||||
const calculateCostTimestamp = selfServeTraceStart(telemetryData);
|
||||
|
||||
try {
|
||||
let costPerHour = 0;
|
||||
for (const region of regions) {
|
||||
@@ -215,14 +228,22 @@ const calculateCost = (skuName: string, instanceCount: number): Description => {
|
||||
costPerHour += incrementalCost;
|
||||
}
|
||||
|
||||
if (costPerHour === 0) {
|
||||
throw new Error("Cost per hour = 0");
|
||||
}
|
||||
|
||||
costPerHour *= instanceCount;
|
||||
costPerHour = Math.round(costPerHour * 100) / 100;
|
||||
|
||||
selfServeTraceSuccess(telemetryData, calculateCostTimestamp);
|
||||
return {
|
||||
textTKey: `${costPerHour} USD`,
|
||||
type: DescriptionType.Text,
|
||||
};
|
||||
} catch (err) {
|
||||
const failureTelemetry = { err, regions, priceMap, selfServeClassName: SqlX.name };
|
||||
selfServeTraceFailure(failureTelemetry, calculateCostTimestamp);
|
||||
|
||||
return costPerHourDefaultValue;
|
||||
}
|
||||
};
|
||||
@@ -324,7 +345,7 @@ export default class SqlX extends SelfServeBaseClass {
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
regions = await getReadRegions();
|
||||
regions = await getRegions();
|
||||
priceMap = await getPriceMap(regions);
|
||||
|
||||
const response = await getCurrentProvisioningState();
|
||||
|
||||
@@ -125,7 +125,8 @@ export class OfferPricing {
|
||||
S3Price: 0.1344,
|
||||
Standard: {
|
||||
StartingPrice: 24 / hoursInAMonth, // per hour
|
||||
PricePerRU: 0.00008,
|
||||
SingleMasterPricePerRU: 0.00008,
|
||||
MultiMasterPricePerRU: 0.00016,
|
||||
PricePerGB: 0.25 / hoursInAMonth,
|
||||
},
|
||||
},
|
||||
@@ -137,7 +138,8 @@ export class OfferPricing {
|
||||
S3Price: 0.6,
|
||||
Standard: {
|
||||
StartingPrice: OfferPricing.MonthlyPricing.mooncake.Standard.StartingPrice / hoursInAMonth, // per hour
|
||||
PricePerRU: 0.00051,
|
||||
SingleMasterPricePerRU: 0.00051,
|
||||
MultiMasterPricePerRU: 0.00102,
|
||||
PricePerGB: OfferPricing.MonthlyPricing.mooncake.Standard.PricePerGB / hoursInAMonth,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as Constants from "./Constants";
|
||||
|
||||
export function computeRUUsagePrice(serverId: string, requestUnits: number): string {
|
||||
if (serverId === "mooncake") {
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.PricePerRU;
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.mooncake.Standard.SingleMasterPricePerRU;
|
||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.mooncake.Currency;
|
||||
}
|
||||
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.PricePerRU;
|
||||
const ruCharge = requestUnits * Constants.OfferPricing.HourlyPricing.default.Standard.SingleMasterPricePerRU;
|
||||
return calculateEstimateNumber(ruCharge) + " " + Constants.OfferPricing.HourlyPricing.default.Currency;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import * as Constants from "../Common/Constants";
|
||||
|
||||
export const manualToAutoscaleDisclaimer = `The starting autoscale max RU/s will be determined by the system, based on the current manual throughput settings and storage of your resource. After autoscale has been enabled, you can change the max RU/s. <a href="${Constants.Urls.autoscaleMigration}">Learn more</a>.`;
|
||||
|
||||
export const minAutoPilotThroughput = 4000;
|
||||
|
||||
export const autoPilotIncrementStep = 1000;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { useDialog } from "../Explorer/Controls/Dialog";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "./GalleryUtils";
|
||||
|
||||
@@ -29,12 +30,12 @@ describe("GalleryUtils", () => {
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in data explorer", () => {
|
||||
const container = {} as Explorer;
|
||||
const container = new Explorer();
|
||||
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(useDialog.getState().visible).toBe(true);
|
||||
expect(useDialog.getState().dialogProps).toBeDefined();
|
||||
expect(useDialog.getState().dialogProps.title).toBe("Download to My Notebooks");
|
||||
expect(useDialog.getState().dialogProps.title).toBe(`Download to ${useNotebook.getState().notebookFolderName}`);
|
||||
});
|
||||
|
||||
it("favoriteItem favorites item", async () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
SortBy,
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import Explorer from "../Explorer/Explorer";
|
||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||
import { useNotebook } from "../Explorer/Notebook/useNotebook";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
@@ -224,68 +225,90 @@ export function downloadItem(
|
||||
|
||||
const name = data.name;
|
||||
useDialog.getState().showOkCancelModalDialog(
|
||||
`Download to ${useNotebook.getState().NotebookFolderName}`,
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
`Download to ${useNotebook.getState().notebookFolderName}`,
|
||||
undefined,
|
||||
"Download",
|
||||
async () => {
|
||||
const clearInProgressMessage = logConsoleProgress(
|
||||
`Downloading ${name} to ${useNotebook.getState().NotebookFolderName}`
|
||||
if (NotebookUtil.isPhoenixEnabled()) {
|
||||
await container.allocateContainer();
|
||||
}
|
||||
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||
if (notebookServerInfo && notebookServerInfo.notebookServerEndpoint !== undefined) {
|
||||
downloadNotebookItem(name, data, junoClient, container, onComplete);
|
||||
} else {
|
||||
useDialog
|
||||
.getState()
|
||||
.showOkModalDialog(
|
||||
"Failed to Connect",
|
||||
"Failed to connect to temporary workspace. Please refresh the page and try again."
|
||||
);
|
||||
}
|
||||
},
|
||||
"Cancel",
|
||||
undefined,
|
||||
container.getDownloadModalConent(name)
|
||||
);
|
||||
}
|
||||
export async function downloadNotebookItem(
|
||||
fileName: string,
|
||||
data: IGalleryItem,
|
||||
junoClient: JunoClient,
|
||||
container: Explorer,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
) {
|
||||
const clearInProgressMessage = logConsoleProgress(
|
||||
`Downloading ${fileName} to ${useNotebook.getState().notebookFolderName}`
|
||||
);
|
||||
const startKey = traceStart(Action.NotebooksGalleryDownload, {
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
const notebook = JSON.parse(response.data) as Notebook;
|
||||
removeNotebookViewerLink(notebook, data.newCellId);
|
||||
|
||||
if (!data.isSample) {
|
||||
const metadata = notebook.metadata as { [name: string]: unknown };
|
||||
metadata.untrusted = true;
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
|
||||
logConsoleInfo(`Successfully downloaded ${data.name} to ${useNotebook.getState().notebookFolderName}`);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
|
||||
startKey
|
||||
);
|
||||
const startKey = traceStart(Action.NotebooksGalleryDownload, {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
});
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
|
||||
}
|
||||
|
||||
const notebook = JSON.parse(response.data) as Notebook;
|
||||
removeNotebookViewerLink(notebook, data.newCellId);
|
||||
|
||||
if (!data.isSample) {
|
||||
const metadata = notebook.metadata as { [name: string]: unknown };
|
||||
metadata.untrusted = true;
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, JSON.stringify(notebook));
|
||||
logConsoleInfo(`Successfully downloaded ${name} to My Notebooks`);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
traceSuccess(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads, isSample: data.isSample },
|
||||
startKey
|
||||
);
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
traceFailure(
|
||||
Action.NotebooksGalleryDownload,
|
||||
{
|
||||
notebookId: data.id,
|
||||
downloadCount: data.downloads,
|
||||
isSample: data.isSample,
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
|
||||
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
|
||||
}
|
||||
|
||||
clearInProgressMessage();
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
clearInProgressMessage();
|
||||
}
|
||||
|
||||
export const removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
|
||||
if (!newCellId) {
|
||||
return;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user