mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-26 12:21:23 +00:00
Compare commits
11 Commits
accessibil
...
users/tara
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de89fd2011 | ||
|
|
5ee2af75dd | ||
|
|
586ae376a3 | ||
|
|
5de6c46a93 | ||
|
|
d712327653 | ||
|
|
e2d34d5131 | ||
|
|
d8bff694c6 | ||
|
|
7fbc33e82b | ||
|
|
c7c0a3a979 | ||
|
|
6ba2ff6000 | ||
|
|
d7718c42da |
@@ -249,6 +249,7 @@ export class HttpHeaders {
|
|||||||
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
public static partitionKey: string = "x-ms-documentdb-partitionkey";
|
||||||
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
public static migrateOfferToManualThroughput: string = "x-ms-cosmos-migrate-offer-to-manual-throughput";
|
||||||
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
public static migrateOfferToAutopilot: string = "x-ms-cosmos-migrate-offer-to-autopilot";
|
||||||
|
public static xAPIKey: string = "X-API-Key";
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ContentType {
|
export class ContentType {
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export interface ConfigContext {
|
|||||||
ARM_API_VERSION: string;
|
ARM_API_VERSION: string;
|
||||||
GRAPH_ENDPOINT: string;
|
GRAPH_ENDPOINT: string;
|
||||||
GRAPH_API_VERSION: string;
|
GRAPH_API_VERSION: string;
|
||||||
|
CATALOG_ENDPOINT: string;
|
||||||
|
CATALOG_API_VERSION: string;
|
||||||
|
CATALOG_API_KEY: string;
|
||||||
ARCADIA_ENDPOINT: string;
|
ARCADIA_ENDPOINT: string;
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: string;
|
||||||
BACKEND_ENDPOINT?: string;
|
BACKEND_ENDPOINT?: string;
|
||||||
@@ -93,6 +96,9 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
ARM_API_VERSION: "2016-06-01",
|
ARM_API_VERSION: "2016-06-01",
|
||||||
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
GRAPH_ENDPOINT: "https://graph.microsoft.com",
|
||||||
GRAPH_API_VERSION: "1.6",
|
GRAPH_API_VERSION: "1.6",
|
||||||
|
CATALOG_ENDPOINT: "https://catalogapi.azure.com/",
|
||||||
|
CATALOG_API_VERSION: "2023-05-01-preview",
|
||||||
|
CATALOG_API_KEY: "",
|
||||||
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net",
|
||||||
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net",
|
||||||
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306
|
||||||
@@ -102,13 +108,13 @@ let configContext: Readonly<ConfigContext> = {
|
|||||||
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod,
|
||||||
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod,
|
||||||
NEW_MONGO_APIS: [
|
NEW_MONGO_APIS: [
|
||||||
// "resourcelist",
|
"resourcelist",
|
||||||
// "queryDocuments",
|
"queryDocuments",
|
||||||
// "createDocument",
|
"createDocument",
|
||||||
// "readDocument",
|
"readDocument",
|
||||||
// "updateDocument",
|
"updateDocument",
|
||||||
// "deleteDocument",
|
"deleteDocument",
|
||||||
// "createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
"legacyMongoShell",
|
"legacyMongoShell",
|
||||||
],
|
],
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
|
|||||||
@@ -420,6 +420,7 @@ export interface SelfServeFrameInputs {
|
|||||||
authorizationToken: string;
|
authorizationToken: string;
|
||||||
csmEndpoint: string;
|
csmEndpoint: string;
|
||||||
flights?: readonly string[];
|
flights?: readonly string[];
|
||||||
|
catalogAPIKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MonacoEditorSettings {
|
export class MonacoEditorSettings {
|
||||||
|
|||||||
@@ -132,16 +132,13 @@ export const createCollectionContextMenuButton = (
|
|||||||
if (configContext.platform !== Platform.Fabric) {
|
if (configContext.platform !== Platform.Fabric) {
|
||||||
items.push({
|
items.push({
|
||||||
iconSrc: DeleteCollectionIcon,
|
iconSrc: DeleteCollectionIcon,
|
||||||
onClick: (lastFocusedElement?: React.RefObject<HTMLElement>) => {
|
onClick: () => {
|
||||||
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
useSelectedNode.getState().setSelectedNode(selectedCollection);
|
||||||
useSidePanel
|
useSidePanel
|
||||||
.getState()
|
.getState()
|
||||||
.openSidePanel(
|
.openSidePanel(
|
||||||
"Delete " + getCollectionName(),
|
"Delete " + getCollectionName(),
|
||||||
<DeleteCollectionConfirmationPane
|
<DeleteCollectionConfirmationPane refreshDatabases={() => container.refreshAllDatabases()} />,
|
||||||
lastFocusedElement={lastFocusedElement}
|
|
||||||
refreshDatabases={() => container.refreshAllDatabases()}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
label: `Delete ${getCollectionName()}`,
|
label: `Delete ${getCollectionName()}`,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
@@ -107,6 +108,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
@@ -223,6 +225,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
"conflictResolutionPolicy": [Function],
|
"conflictResolutionPolicy": [Function],
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
@@ -269,6 +272,7 @@ exports[`SettingsComponent renders 1`] = `
|
|||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcesso
|
|||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
onClick: (value?: React.RefObject<any>) => void;
|
onClick: () => void;
|
||||||
iconSrc?: string;
|
iconSrc?: string;
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
styleClass?: string;
|
styleClass?: string;
|
||||||
@@ -242,9 +242,8 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
<div ref={this.contextMenuRef} onContextMenu={this.onRightClick} onKeyPress={this.onMoreButtonKeyPress}>
|
||||||
<IconButton
|
<IconButton
|
||||||
elementRef={this.contextMenuRef}
|
|
||||||
name="More"
|
name="More"
|
||||||
title="More"
|
title="More"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
@@ -284,7 +283,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
|||||||
disabled: menuItem.isDisabled,
|
disabled: menuItem.isDisabled,
|
||||||
className: menuItem.styleClass,
|
className: menuItem.styleClass,
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
menuItem.onClick(this.contextMenuRef);
|
menuItem.onClick();
|
||||||
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||||
label: menuItem.label,
|
label: menuItem.label,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -174,11 +174,6 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
|||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More options"
|
ariaLabel="More options"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
elementRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "More",
|
"iconName": "More",
|
||||||
@@ -404,11 +399,6 @@ exports[`TreeNodeComponent renders sorted children, expanded, leaves and parents
|
|||||||
<CustomizedIconButton
|
<CustomizedIconButton
|
||||||
ariaLabel="More options"
|
ariaLabel="More options"
|
||||||
className="treeMenuEllipsis"
|
className="treeMenuEllipsis"
|
||||||
elementRef={
|
|
||||||
Object {
|
|
||||||
"current": null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuIconProps={
|
menuIconProps={
|
||||||
Object {
|
Object {
|
||||||
"iconName": "More",
|
"iconName": "More",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { fromContentUri, toRawContentUri } from "../Utils/GitHubUtils";
|
|||||||
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../Utils/NotificationConsoleUtils";
|
||||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||||
|
import { listByDatabaseAccount } from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||||
import { useSidePanel } from "../hooks/useSidePanel";
|
import { useSidePanel } from "../hooks/useSidePanel";
|
||||||
import { useTabs } from "../hooks/useTabs";
|
import { useTabs } from "../hooks/useTabs";
|
||||||
import "./ComponentRegisterer";
|
import "./ComponentRegisterer";
|
||||||
@@ -55,6 +56,7 @@ import { AddCollectionPanel } from "./Panes/AddCollectionPanel";
|
|||||||
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
import { CassandraAddCollectionPane } from "./Panes/CassandraAddCollectionPane/CassandraAddCollectionPane";
|
||||||
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
import { ExecuteSprocParamsPane } from "./Panes/ExecuteSprocParamsPane/ExecuteSprocParamsPane";
|
||||||
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
import { StringInputPane } from "./Panes/StringInputPane/StringInputPane";
|
||||||
|
import { UploadFilePane } from "./Panes/UploadFilePane/UploadFilePane";
|
||||||
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
import { UploadItemsPane } from "./Panes/UploadItemsPane/UploadItemsPane";
|
||||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||||
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
import NotebookV2Tab, { NotebookTabOptions } from "./Tabs/NotebookV2Tab";
|
||||||
@@ -508,6 +510,104 @@ export default class Explorer {
|
|||||||
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
.then((memoryUsageInfo) => useNotebook.getState().setMemoryUsageInfo(memoryUsageInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public resetNotebookWorkspace(): void {
|
||||||
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookClient) {
|
||||||
|
handleError(
|
||||||
|
"Attempt to reset notebook workspace, but notebook is not enabled",
|
||||||
|
"Explorer/resetNotebookWorkspace",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dialogContent = useNotebook.getState().isPhoenixNotebooks
|
||||||
|
? "Notebooks saved in the temporary workspace will be deleted. Do you want to proceed?"
|
||||||
|
: "This lets you keep your notebook files and the workspace will be restored to default. Proceed anyway?";
|
||||||
|
|
||||||
|
const resetConfirmationDialogProps: DialogProps = {
|
||||||
|
isModal: true,
|
||||||
|
title: "Reset Workspace",
|
||||||
|
subText: dialogContent,
|
||||||
|
primaryButtonText: "OK",
|
||||||
|
secondaryButtonText: "Cancel",
|
||||||
|
onPrimaryButtonClick: this._resetNotebookWorkspace,
|
||||||
|
onSecondaryButtonClick: () => useDialog.getState().closeDialog(),
|
||||||
|
};
|
||||||
|
useDialog.getState().openDialog(resetConfirmationDialogProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _containsDefaultNotebookWorkspace(databaseAccount: DataModels.DatabaseAccount): Promise<boolean> {
|
||||||
|
if (!databaseAccount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { value: workspaces } = await listByDatabaseAccount(
|
||||||
|
userContext.subscriptionId,
|
||||||
|
userContext.resourceGroup,
|
||||||
|
userContext.databaseAccount.name,
|
||||||
|
);
|
||||||
|
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
|
||||||
|
} catch (error) {
|
||||||
|
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetNotebookWorkspace = async () => {
|
||||||
|
useDialog.getState().closeDialog();
|
||||||
|
const clearInProgressMessage = logConsoleProgress("Resetting notebook workspace");
|
||||||
|
let connectionStatus: ContainerConnectionInfo;
|
||||||
|
try {
|
||||||
|
const notebookServerInfo = useNotebook.getState().notebookServerInfo;
|
||||||
|
if (!notebookServerInfo || !notebookServerInfo.notebookServerEndpoint) {
|
||||||
|
const error = "No server endpoint detected";
|
||||||
|
Logger.logError(error, "NotebookContainerClient/resetWorkspace");
|
||||||
|
logConsoleError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
TelemetryProcessor.traceStart(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
useTabs.getState().closeAllNotebookTabs(true);
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Connecting,
|
||||||
|
};
|
||||||
|
useNotebook.getState().setConnectionInfo(connectionStatus);
|
||||||
|
}
|
||||||
|
const connectionInfo = await this.notebookManager?.notebookClient.resetWorkspace();
|
||||||
|
if (connectionInfo?.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new Error(`Reset Workspace: Received status code- ${connectionInfo?.status}`);
|
||||||
|
}
|
||||||
|
if (!connectionInfo?.data?.phoenixServiceUrl) {
|
||||||
|
throw new Error(`Reset Workspace: PhoenixServiceUrl is invalid!`);
|
||||||
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
await this.setNotebookInfo(true, connectionInfo, connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
|
logConsoleInfo("Successfully reset notebook workspace");
|
||||||
|
TelemetryProcessor.traceSuccess(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Failed to reset notebook workspace: ${error}`);
|
||||||
|
TelemetryProcessor.traceFailure(Action.PhoenixResetWorkspace, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
error: getErrorMessage(error),
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
});
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
connectionStatus = {
|
||||||
|
status: ConnectionStatusType.Failed,
|
||||||
|
};
|
||||||
|
useNotebook.getState().resetContainerConnection(connectionStatus);
|
||||||
|
useNotebook.getState().setIsRefreshed(!useNotebook.getState().isRefreshed);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearInProgressMessage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private getDeltaDatabases(
|
private getDeltaDatabases(
|
||||||
updatedDatabaseList: DataModels.Database[],
|
updatedDatabaseList: DataModels.Database[],
|
||||||
databases: ViewModels.Database[],
|
databases: ViewModels.Database[],
|
||||||
@@ -910,6 +1010,92 @@ export default class Explorer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This creates a new notebook file, then opens the notebook
|
||||||
|
*/
|
||||||
|
public async onNewNotebookClicked(parent?: NotebookContentItem, isGithubTree?: boolean): Promise<void> {
|
||||||
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
|
const error = "Attempt to create new notebook, but notebook is not enabled";
|
||||||
|
handleError(error, "Explorer/onNewNotebookClicked");
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
if (isGithubTree) {
|
||||||
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
|
this.createNewNoteBook(parent, isGithubTree);
|
||||||
|
} else {
|
||||||
|
useDialog.getState().showOkCancelModalDialog(
|
||||||
|
Notebook.newNotebookModalTitle,
|
||||||
|
undefined,
|
||||||
|
"Create",
|
||||||
|
async () => {
|
||||||
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
|
this.createNewNoteBook(parent, isGithubTree);
|
||||||
|
},
|
||||||
|
"Cancel",
|
||||||
|
undefined,
|
||||||
|
this.getNewNoteWarningText(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parent = parent || this.resourceTree.myNotebooksContentRoot;
|
||||||
|
this.createNewNoteBook(parent, isGithubTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.notebookManager?.notebookContentClient
|
||||||
|
.createNewNotebookFile(parent, isGithubTree)
|
||||||
|
.then((newFile: NotebookContentItem) => {
|
||||||
|
logConsoleInfo(`Successfully created: ${newFile.name}`);
|
||||||
|
TelemetryProcessor.traceSuccess(
|
||||||
|
Action.CreateNewNotebook,
|
||||||
|
{
|
||||||
|
dataExplorerArea: Constants.Areas.Notebook,
|
||||||
|
},
|
||||||
|
startKey,
|
||||||
|
);
|
||||||
|
return this.openNotebook(newFile);
|
||||||
|
})
|
||||||
|
.then(() => this.resourceTree.triggerRender())
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMessage = `Failed to create a new notebook: ${getErrorMessage(error)}`;
|
||||||
|
logConsoleError(errorMessage);
|
||||||
|
TelemetryProcessor.traceFailure(
|
||||||
|
Action.CreateNewNotebook,
|
||||||
|
{
|
||||||
|
dataExplorerArea: Constants.Areas.Notebook,
|
||||||
|
error: errorMessage,
|
||||||
|
errorStack: getErrorStack(error),
|
||||||
|
},
|
||||||
|
startKey,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(clearInProgressMessage);
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
// TODO: Delete this function when ResourceTreeAdapter is removed.
|
||||||
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
public async refreshContentItem(item: NotebookContentItem): Promise<void> {
|
||||||
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
if (!useNotebook.getState().isNotebookEnabled || !this.notebookManager?.notebookContentClient) {
|
||||||
@@ -944,6 +1130,10 @@ export default class Explorer {
|
|||||||
let title: string;
|
let title: string;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
|
case ViewModels.TerminalKind.Default:
|
||||||
|
title = "Terminal";
|
||||||
|
break;
|
||||||
|
|
||||||
case ViewModels.TerminalKind.Mongo:
|
case ViewModels.TerminalKind.Mongo:
|
||||||
title = "Mongo Shell";
|
title = "Mongo Shell";
|
||||||
break;
|
break;
|
||||||
@@ -1097,6 +1287,36 @@ export default class Explorer {
|
|||||||
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
.openSidePanel("Input parameters", <ExecuteSprocParamsPane storedProcedure={storedProcedure} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openUploadFilePanel(parent?: NotebookContentItem): void {
|
||||||
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
useDialog.getState().showOkCancelModalDialog(
|
||||||
|
Notebook.newNotebookUploadModalTitle,
|
||||||
|
undefined,
|
||||||
|
"Upload",
|
||||||
|
async () => {
|
||||||
|
await this.allocateContainer(PoolIdType.DefaultPoolId);
|
||||||
|
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(
|
||||||
|
"Upload file to notebook server",
|
||||||
|
<UploadFilePane uploadFile={(name: string, content: string) => this.uploadFile(name, content, parent)} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public getDownloadModalConent(fileName: string): JSX.Element {
|
public getDownloadModalConent(fileName: string): JSX.Element {
|
||||||
if (useNotebook.getState().isPhoenixNotebooks) {
|
if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
|
|
||||||
describe("shouldRecordFeedback()", () => {
|
describe("shouldRecordFeedback()", () => {
|
||||||
it("should return true if last collection and database does not have shared throughput else false", () => {
|
it("should return true if last collection and database does not have shared throughput else false", () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
expect(wrapper.exists(".deleteCollectionFeedback")).toBe(false);
|
||||||
|
|
||||||
const database = { id: ko.observable("testDB") } as Database;
|
const database = { id: ko.observable("testDB") } as Database;
|
||||||
@@ -111,9 +109,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call delete collection", () => {
|
it("should call delete collection", () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
|
||||||
);
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
|
||||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||||
@@ -130,9 +126,7 @@ describe("Delete Collection Confirmation Pane", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should record feedback", async () => {
|
it("should record feedback", async () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} />);
|
||||||
<DeleteCollectionConfirmationPane refreshDatabases={() => undefined} lastFocusedElement={undefined} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
expect(wrapper.exists("#confirmCollectionId")).toBe(true);
|
||||||
wrapper
|
wrapper
|
||||||
.find("#confirmCollectionId")
|
.find("#confirmCollectionId")
|
||||||
|
|||||||
@@ -12,19 +12,17 @@ import { getCollectionName } from "Utils/APITypeUtils";
|
|||||||
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "Utils/NotificationConsoleUtils";
|
||||||
import { useSidePanel } from "hooks/useSidePanel";
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
import { useTabs } from "hooks/useTabs";
|
import { useTabs } from "hooks/useTabs";
|
||||||
import React, { FunctionComponent, useEffect, useState } from "react";
|
import React, { FunctionComponent, useState } from "react";
|
||||||
import { useDatabases } from "../../useDatabases";
|
import { useDatabases } from "../../useDatabases";
|
||||||
import { useSelectedNode } from "../../useSelectedNode";
|
import { useSelectedNode } from "../../useSelectedNode";
|
||||||
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
export interface DeleteCollectionConfirmationPaneProps {
|
export interface DeleteCollectionConfirmationPaneProps {
|
||||||
refreshDatabases: () => Promise<void>;
|
refreshDatabases: () => Promise<void>;
|
||||||
lastFocusedElement: React.MutableRefObject<HTMLElement>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectionConfirmationPaneProps> = ({
|
||||||
refreshDatabases,
|
refreshDatabases,
|
||||||
lastFocusedElement,
|
|
||||||
}: DeleteCollectionConfirmationPaneProps) => {
|
}: DeleteCollectionConfirmationPaneProps) => {
|
||||||
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
const [deleteCollectionFeedback, setDeleteCollectionFeedback] = useState<string>("");
|
||||||
@@ -37,7 +35,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
|
|
||||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||||
const paneTitle = "Delete " + collectionName;
|
const paneTitle = "Delete " + collectionName;
|
||||||
const lastItemElement = lastFocusedElement?.current;
|
|
||||||
|
|
||||||
const onSubmit = async (): Promise<void> => {
|
const onSubmit = async (): Promise<void> => {
|
||||||
const collection = useSelectedNode.getState().findSelectedCollection();
|
const collection = useSelectedNode.getState().findSelectedCollection();
|
||||||
@@ -114,13 +111,6 @@ export const DeleteCollectionConfirmationPane: FunctionComponent<DeleteCollectio
|
|||||||
};
|
};
|
||||||
const confirmContainer = `Confirm by typing the ${collectionName.toLowerCase()} id`;
|
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}?`;
|
const reasonInfo = `Help us improve Azure Cosmos DB! What is the reason why you are deleting this ${collectionName}?`;
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (lastItemElement) {
|
|
||||||
lastItemElement.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [lastItemElement]);
|
|
||||||
return (
|
return (
|
||||||
<RightPaneForm {...props}>
|
<RightPaneForm {...props}>
|
||||||
<div className="panelFormWrapper">
|
<div className="panelFormWrapper">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ exports[`GitHub Repos Panel should render Default properly 1`] = `
|
|||||||
Object {
|
Object {
|
||||||
"container": Explorer {
|
"container": Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports[`StringInput Pane should render Create new directory properly 1`] = `
|
|||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|||||||
91
src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx
Normal file
91
src/Explorer/Panes/UploadFilePane/UploadFilePane.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Upload } from "Common/Upload/Upload";
|
||||||
|
import { useSidePanel } from "hooks/useSidePanel";
|
||||||
|
import React, { ChangeEvent, FunctionComponent, useState } from "react";
|
||||||
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "Utils/NotificationConsoleUtils";
|
||||||
|
import { NotebookContentItem } from "../../Notebook/NotebookContentItem";
|
||||||
|
import { RightPaneForm, RightPaneFormProps } from "../RightPaneForm/RightPaneForm";
|
||||||
|
|
||||||
|
export interface UploadFilePanelProps {
|
||||||
|
uploadFile: (name: string, content: string) => Promise<NotebookContentItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadFilePane: FunctionComponent<UploadFilePanelProps> = ({ uploadFile }: UploadFilePanelProps) => {
|
||||||
|
const closeSidePanel = useSidePanel((state) => state.closeSidePanel);
|
||||||
|
const extensions: string = undefined; //ex. ".ipynb"
|
||||||
|
const errorMessage = "Could not upload file";
|
||||||
|
const inProgressMessage = "Uploading file to notebook server";
|
||||||
|
const successMessage = "Successfully uploaded file to notebook server";
|
||||||
|
|
||||||
|
const [files, setFiles] = useState<FileList>();
|
||||||
|
const [formErrors, setFormErrors] = useState<string>("");
|
||||||
|
const [isExecuting, setIsExecuting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
setFormErrors("");
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
setFormErrors("No file specified. Please input a file.");
|
||||||
|
logConsoleError(`${errorMessage} -- No file specified. Please input a file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: File = files.item(0);
|
||||||
|
|
||||||
|
const clearMessage = logConsoleProgress(`${inProgressMessage}: ${file.name}`);
|
||||||
|
|
||||||
|
setIsExecuting(true);
|
||||||
|
|
||||||
|
onSubmit(files.item(0))
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
logConsoleInfo(`${successMessage} ${file.name}`);
|
||||||
|
closeSidePanel();
|
||||||
|
},
|
||||||
|
(error: string) => {
|
||||||
|
setFormErrors(errorMessage);
|
||||||
|
logConsoleError(`${errorMessage} ${file.name}: ${error}`);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.finally(() => {
|
||||||
|
setIsExecuting(false);
|
||||||
|
clearMessage();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSelectedFiles = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
setFiles(event.target.files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (file: File): Promise<NotebookContentItem> => {
|
||||||
|
const readFileAsText = (inputFile: File): Promise<string> => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
reader.onerror = () => {
|
||||||
|
reader.abort();
|
||||||
|
reject(`Problem parsing file: ${inputFile}`);
|
||||||
|
};
|
||||||
|
reader.onload = () => {
|
||||||
|
resolve(reader.result as string);
|
||||||
|
};
|
||||||
|
reader.readAsText(inputFile);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileContent = await readFileAsText(file);
|
||||||
|
return uploadFile(file.name, fileContent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: RightPaneFormProps = {
|
||||||
|
formError: formErrors,
|
||||||
|
isExecuting: isExecuting,
|
||||||
|
submitButtonText: "Upload",
|
||||||
|
onSubmit: submit,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RightPaneForm {...props}>
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<Upload label="Select file to upload" accept={extensions} onUpload={updateSelectedFiles} />
|
||||||
|
</div>
|
||||||
|
</RightPaneForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ exports[`Query copilot tab snapshot test should render with initial input 1`] =
|
|||||||
explorer={
|
explorer={
|
||||||
Explorer {
|
Explorer {
|
||||||
"_isInitializingNotebooks": false,
|
"_isInitializingNotebooks": false,
|
||||||
|
"_resetNotebookWorkspace": [Function],
|
||||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||||
"isTabsContentExpanded": [Function],
|
"isTabsContentExpanded": [Function],
|
||||||
"onRefreshDatabasesKeyPress": [Function],
|
"onRefreshDatabasesKeyPress": [Function],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import * as React from "react";
|
|||||||
import ConnectIcon from "../../../images/Connect_color.svg";
|
import ConnectIcon from "../../../images/Connect_color.svg";
|
||||||
import ContainersIcon from "../../../images/Containers.svg";
|
import ContainersIcon from "../../../images/Containers.svg";
|
||||||
import LinkIcon from "../../../images/Link_blue.svg";
|
import LinkIcon from "../../../images/Link_blue.svg";
|
||||||
|
import NotebookColorIcon from "../../../images/Notebooks.svg";
|
||||||
import PowerShellIcon from "../../../images/PowerShell.svg";
|
import PowerShellIcon from "../../../images/PowerShell.svg";
|
||||||
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
import CopilotIcon from "../../../images/QueryCopilotNewLogo.svg";
|
||||||
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
import QuickStartIcon from "../../../images/Quickstart_Lightning.svg";
|
||||||
@@ -409,6 +410,14 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
heroes.push(launchQuickstartBtn);
|
heroes.push(launchQuickstartBtn);
|
||||||
|
} else if (useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
const newNotebookBtn = {
|
||||||
|
iconSrc: NotebookColorIcon,
|
||||||
|
title: "New notebook",
|
||||||
|
description: "Visualize your data stored in Azure Cosmos DB",
|
||||||
|
onClick: () => this.container.onNewNotebookClicked(),
|
||||||
|
};
|
||||||
|
heroes.push(newNotebookBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
heroes.push(this.getShellCard());
|
heroes.push(this.getShellCard());
|
||||||
@@ -680,20 +689,11 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
title: "Learn the Fundamentals",
|
title: "Learn the Fundamentals",
|
||||||
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
description: "Watch Azure Cosmos DB Live TV show introductory and how to videos.",
|
||||||
};
|
};
|
||||||
|
let items: item[];
|
||||||
const commonItems: item[] = [
|
|
||||||
{
|
|
||||||
link: "https://learn.microsoft.com/azure/cosmos-db/data-explorer-shortcuts",
|
|
||||||
title: "Data Explorer keyboard shortcuts",
|
|
||||||
description: "Learn keyboard shortcuts to navigate Data Explorer.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let apiItems: item[];
|
|
||||||
switch (userContext.apiType) {
|
switch (userContext.apiType) {
|
||||||
case "SQL":
|
case "SQL":
|
||||||
case "Postgres":
|
case "Postgres":
|
||||||
apiItems = [
|
items = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/msl-sdk-connect",
|
link: "https://aka.ms/msl-sdk-connect",
|
||||||
title: "Get Started using an SDK",
|
title: "Get Started using an SDK",
|
||||||
@@ -708,7 +708,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Mongo":
|
case "Mongo":
|
||||||
apiItems = [
|
items = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/mongonodejs",
|
link: "https://aka.ms/mongonodejs",
|
||||||
title: "Build an app with Node.js",
|
title: "Build an app with Node.js",
|
||||||
@@ -723,7 +723,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Cassandra":
|
case "Cassandra":
|
||||||
apiItems = [
|
items = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/cassandracontainer",
|
link: "https://aka.ms/cassandracontainer",
|
||||||
title: "Create a Container",
|
title: "Create a Container",
|
||||||
@@ -738,7 +738,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Gremlin":
|
case "Gremlin":
|
||||||
apiItems = [
|
items = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/graphquickstart",
|
link: "https://aka.ms/graphquickstart",
|
||||||
title: "Get Started ",
|
title: "Get Started ",
|
||||||
@@ -753,7 +753,7 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case "Tables":
|
case "Tables":
|
||||||
apiItems = [
|
items = [
|
||||||
{
|
{
|
||||||
link: "https://aka.ms/tabledotnet",
|
link: "https://aka.ms/tabledotnet",
|
||||||
title: "Build a .NET App",
|
title: "Build a .NET App",
|
||||||
@@ -770,9 +770,6 @@ export class SplashScreen extends React.Component<SplashScreenProps> {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = [...commonItems, ...apiItems];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
|
|||||||
@@ -80,8 +80,7 @@
|
|||||||
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
|
placeholder:isPreferredApiMongoDB?'Type a query predicate (e.g., {´a´:´foo´}), or choose one from the drop down list, or leave empty to query all documents.':'Type a query predicate (e.g., WHERE c.id=´1´), or choose one from the drop down list, or leave empty to query all documents.'
|
||||||
},
|
},
|
||||||
css: { placeholderVisible: filterContent().length === 0 },
|
css: { placeholderVisible: filterContent().length === 0 },
|
||||||
textInput: filterContent,
|
textInput: filterContent"
|
||||||
event: { keydown: onFilterKeyDown }"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<datalist
|
<datalist
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
import { ItemDefinition, PartitionKey, PartitionKeyDefinition, QueryIterator, Resource } from "@azure/cosmos";
|
||||||
import { Platform, configContext } from "ConfigContext";
|
import { Platform, configContext } from "ConfigContext";
|
||||||
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
import { querySampleDocuments, readSampleDocument } from "Explorer/QueryCopilot/QueryCopilotUtilities";
|
||||||
import { KeyboardAction, KeyboardActionGroup, KeyboardHandlerSetter, useKeyboardActionGroup } from "KeyboardShortcuts";
|
import { KeyboardAction } from "KeyboardShortcuts";
|
||||||
import { QueryConstants } from "Shared/Constants";
|
import { QueryConstants } from "Shared/Constants";
|
||||||
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
import { LocalStorageUtility, StorageKey } from "Shared/StorageUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
@@ -86,11 +86,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
private _isQueryCopilotSampleContainer: boolean;
|
private _isQueryCopilotSampleContainer: boolean;
|
||||||
private queryAbortController: AbortController;
|
private queryAbortController: AbortController;
|
||||||
private cancelQueryTimeoutID: NodeJS.Timeout;
|
private cancelQueryTimeoutID: NodeJS.Timeout;
|
||||||
private setKeyboardActions: KeyboardHandlerSetter;
|
|
||||||
|
|
||||||
constructor(options: ViewModels.DocumentsTabOptions) {
|
constructor(options: ViewModels.DocumentsTabOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
this.setKeyboardActions = useKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
|
||||||
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
this.isPreferredApiMongoDB = userContext.apiType === "Mongo" || options.isPreferredApiMongoDB;
|
||||||
|
|
||||||
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
this.idHeader = this.isPreferredApiMongoDB ? "_id" : "id";
|
||||||
@@ -667,38 +665,9 @@ export default class DocumentsTab extends TabsBase {
|
|||||||
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
this.collection && this.collection.selectedSubnodeKind(ViewModels.CollectionTabKind.Documents);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFilterKeyDown(model: unknown, e: KeyboardEvent): boolean {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
this.refreshDocumentsGrid(true);
|
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
|
||||||
return false;
|
|
||||||
} else if (e.key === "Escape") {
|
|
||||||
this.onHideFilterClick();
|
|
||||||
|
|
||||||
// Suppress the default behavior of the key
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// Allow the default behavior of the key
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async onActivate(): Promise<void> {
|
public async onActivate(): Promise<void> {
|
||||||
super.onActivate();
|
super.onActivate();
|
||||||
|
|
||||||
this.setKeyboardActions({
|
|
||||||
[KeyboardAction.SEARCH]: () => {
|
|
||||||
this.onShowFilterClick();
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[KeyboardAction.CLEAR_SEARCH]: () => {
|
|
||||||
this.filterContent("");
|
|
||||||
this.refreshDocumentsGrid(true);
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this._documentsIterator) {
|
if (!this._documentsIterator) {
|
||||||
try {
|
try {
|
||||||
await this.autoPopulateContent();
|
await this.autoPopulateContent();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { KeyboardActionGroup, clearKeyboardActionGroup } from "KeyboardShortcuts";
|
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as Constants from "../../Common/Constants";
|
import * as Constants from "../../Common/Constants";
|
||||||
import * as ThemeUtility from "../../Common/ThemeUtility";
|
import * as ThemeUtility from "../../Common/ThemeUtility";
|
||||||
@@ -108,7 +107,6 @@ export default class TabsBase extends WaitsForTemplateViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onActivate(): void {
|
public onActivate(): void {
|
||||||
clearKeyboardActionGroup(KeyboardActionGroup.ACTIVE_TAB);
|
|
||||||
this.updateSelectedNode();
|
this.updateSelectedNode();
|
||||||
this.collection?.selectedSubnodeKind(this.tabKind);
|
this.collection?.selectedSubnodeKind(this.tabKind);
|
||||||
this.database?.selectedSubnodeKind(this.tabKind);
|
this.database?.selectedSubnodeKind(this.tabKind);
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
import { SampleDataTree } from "Explorer/Tree/SampleDataTree";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
import { useQueryCopilot } from "hooks/useQueryCopilot";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import shallow from "zustand/shallow";
|
import shallow from "zustand/shallow";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||||
@@ -12,14 +14,17 @@ import FileIcon from "../../../images/notebook/file-cosmos.svg";
|
|||||||
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
import PublishIcon from "../../../images/notebook/publish_content.svg";
|
||||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
|
import { Areas, ConnectionStatusType, Notebook } from "../../Common/Constants";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
@@ -31,6 +36,7 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
@@ -69,6 +75,152 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
configContext.platform !== Platform.Fabric && (userContext.apiType === "SQL" || userContext.apiType === "Gremlin");
|
||||||
const pseudoDirPath = "PsuedoDir";
|
const pseudoDirPath = "PsuedoDir";
|
||||||
|
|
||||||
|
const buildGalleryCallout = (): JSX.Element => {
|
||||||
|
if (
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calloutProps: ICalloutProps = {
|
||||||
|
calloutMaxWidth: 350,
|
||||||
|
ariaLabel: "New gallery",
|
||||||
|
role: "alertdialog",
|
||||||
|
gapSpace: 0,
|
||||||
|
target: ".galleryHeader",
|
||||||
|
directionalHint: DirectionalHint.leftTopEdge,
|
||||||
|
onDismiss: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
},
|
||||||
|
setInitialFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openGalleryProps: ILinkProps = {
|
||||||
|
onClick: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
container.openGallery();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebooksTree = (): TreeNode => {
|
||||||
|
const notebooksTree: TreeNode = {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!useNotebook.getState().isPhoenixNotebooks) {
|
||||||
|
notebooksTree.children.push(buildNotebooksTemporarilyDownTree());
|
||||||
|
} else {
|
||||||
|
if (galleryContentRoot) {
|
||||||
|
notebooksTree.children.push(buildGalleryNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
myNotebooksContentRoot &&
|
||||||
|
useNotebook.getState().isPhoenixNotebooks &&
|
||||||
|
useNotebook.getState().connectionInfo.status === ConnectionStatusType.Connected
|
||||||
|
) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildNotebooksTemporarilyDownTree = (): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: Notebook.temporarilyDownMsg,
|
||||||
|
className: "clickDisabled",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGalleryNotebooksTree = (): TreeNode => {
|
||||||
|
return {
|
||||||
|
label: "Gallery",
|
||||||
|
iconSrc: GalleryIcon,
|
||||||
|
className: "notebookHeader galleryHeader",
|
||||||
|
onClick: () => container.openGallery(),
|
||||||
|
isSelected: () => activeTab?.tabKind === ViewModels.CollectionTabKind.Gallery,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMyNotebooksTree = (): TreeNode => {
|
||||||
|
const myNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||||
|
myNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
container.openNotebook(item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
myNotebooksTree.isExpanded = true;
|
||||||
|
myNotebooksTree.isAlphaSorted = true;
|
||||||
|
// Remove "Delete" menu item from context menu
|
||||||
|
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
|
||||||
|
return myNotebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildGitHubNotebooksTree = (isConnected: boolean): TreeNode => {
|
||||||
|
const gitHubNotebooksTree: TreeNode = buildNotebookDirectoryNode(
|
||||||
|
gitHubNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
container.openNotebook(item);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const manageGitContextMenu: TreeNodeMenuItem[] = [
|
||||||
|
{
|
||||||
|
label: "Manage GitHub settings",
|
||||||
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={container}
|
||||||
|
gitHubClientProp={container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={container.notebookManager.junoClient}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Disconnect from GitHub",
|
||||||
|
onClick: () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
container.notebookManager?.gitHubOAuthService.logout();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
gitHubNotebooksTree.contextMenu = manageGitContextMenu;
|
||||||
|
gitHubNotebooksTree.isExpanded = true;
|
||||||
|
gitHubNotebooksTree.isAlphaSorted = true;
|
||||||
|
|
||||||
|
return gitHubNotebooksTree;
|
||||||
|
};
|
||||||
|
|
||||||
const buildChildNodes = (
|
const buildChildNodes = (
|
||||||
item: NotebookContentItem,
|
item: NotebookContentItem,
|
||||||
onFileClick: (item: NotebookContentItem) => void,
|
onFileClick: (item: NotebookContentItem) => void,
|
||||||
@@ -221,6 +373,11 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
onClick: () => container.onCreateDirectory(item, isGithubTree),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Upload File",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => container.openUploadFilePanel(item),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
//disallow renaming of temporary notebook workspace
|
//disallow renaming of temporary notebook workspace
|
||||||
@@ -625,6 +782,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isNotebookEnabled && isSampleDataEnabled && (
|
{!isNotebookEnabled && isSampleDataEnabled && (
|
||||||
@@ -637,6 +796,8 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isNotebookEnabled && isSampleDataEnabled && (
|
{isNotebookEnabled && isSampleDataEnabled && (
|
||||||
@@ -648,7 +809,12 @@ export const ResourceTree: React.FC<ResourceTreeProps> = ({ container }: Resourc
|
|||||||
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
<AccordionItemComponent title={"SAMPLE DATA"} containerStyles={{ display: "table" }}>
|
||||||
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
<SampleDataTree sampleDataResourceTokenCollection={sampleDataResourceTokenCollection} />
|
||||||
</AccordionItemComponent>
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={"NOTEBOOKS"}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={buildNotebooksTree()} />
|
||||||
|
</AccordionItemComponent>
|
||||||
</AccordionComponent>
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {buildGalleryCallout()} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { Callout, DirectionalHint, ICalloutProps, ILinkProps, Link, Stack, Text } from "@fluentui/react";
|
||||||
import { getItemName } from "Utils/APITypeUtils";
|
import { getItemName } from "Utils/APITypeUtils";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
|
import GalleryIcon from "../../../images/GalleryIcon.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
|
||||||
@@ -11,17 +13,21 @@ import PublishIcon from "../../../images/notebook/publish_content.svg";
|
|||||||
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
import RefreshIcon from "../../../images/refresh-cosmos.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import { Areas } from "../../Common/Constants";
|
||||||
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility";
|
||||||
import * as DataModels from "../../Contracts/DataModels";
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import { LocalStorageUtility, StorageKey } from "../../Shared/StorageUtility";
|
||||||
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
import { Action, ActionModifiers, Source } from "../../Shared/Telemetry/TelemetryConstants";
|
||||||
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
import { isServerlessAccount } from "../../Utils/CapabilityUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { useSidePanel } from "../../hooks/useSidePanel";
|
||||||
import { useTabs } from "../../hooks/useTabs";
|
import { useTabs } from "../../hooks/useTabs";
|
||||||
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory";
|
||||||
|
import { AccordionComponent, AccordionItemComponent } from "../Controls/Accordion/AccordionComponent";
|
||||||
import { useDialog } from "../Controls/Dialog";
|
import { useDialog } from "../Controls/Dialog";
|
||||||
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
import { TreeComponent, TreeNode, TreeNodeMenuItem } from "../Controls/TreeComponent/TreeComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
@@ -30,6 +36,7 @@ import { mostRecentActivity } from "../MostRecentActivity/MostRecentActivity";
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
import { useNotebook } from "../Notebook/useNotebook";
|
import { useNotebook } from "../Notebook/useNotebook";
|
||||||
|
import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel";
|
||||||
import TabsBase from "../Tabs/TabsBase";
|
import TabsBase from "../Tabs/TabsBase";
|
||||||
import { useDatabases } from "../useDatabases";
|
import { useDatabases } from "../useDatabases";
|
||||||
import { useSelectedNode } from "../useSelectedNode";
|
import { useSelectedNode } from "../useSelectedNode";
|
||||||
@@ -95,7 +102,26 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
const dataRootNode = this.buildDataTree();
|
const dataRootNode = this.buildDataTree();
|
||||||
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
const notebooksRootNode = this.buildNotebooksTrees();
|
||||||
|
|
||||||
|
if (useNotebook.getState().isNotebookEnabled) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionComponent>
|
||||||
|
<AccordionItemComponent title={ResourceTreeAdapter.DataTitle} isExpanded={!this.gitHubNotebooksContentRoot}>
|
||||||
|
<TreeComponent className="dataResourceTree" rootNode={dataRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
<AccordionItemComponent title={ResourceTreeAdapter.NotebooksTitle}>
|
||||||
|
<TreeComponent className="notebookResourceTree" rootNode={notebooksRootNode} />
|
||||||
|
</AccordionItemComponent>
|
||||||
|
</AccordionComponent>
|
||||||
|
|
||||||
|
{/* {this.galleryContentRoot && this.buildGalleryCallout()} */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <TreeComponent className="dataResourceTree" rootNode={dataRootNode} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initialize(): Promise<void[]> {
|
public async initialize(): Promise<void[]> {
|
||||||
@@ -478,6 +504,156 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
return traverse(schema);
|
return traverse(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildNotebooksTrees(): TreeNode {
|
||||||
|
let notebooksTree: TreeNode = {
|
||||||
|
label: undefined,
|
||||||
|
isExpanded: true,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.galleryContentRoot) {
|
||||||
|
notebooksTree.children.push(this.buildGalleryNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.myNotebooksContentRoot) {
|
||||||
|
notebooksTree.children.push(this.buildMyNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.gitHubNotebooksContentRoot) {
|
||||||
|
// collapse all other notebook nodes
|
||||||
|
notebooksTree.children.forEach((node) => (node.isExpanded = false));
|
||||||
|
notebooksTree.children.push(this.buildGitHubNotebooksTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
return notebooksTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGalleryCallout(): JSX.Element {
|
||||||
|
if (
|
||||||
|
LocalStorageUtility.hasItem(StorageKey.GalleryCalloutDismissed) &&
|
||||||
|
LocalStorageUtility.getEntryBoolean(StorageKey.GalleryCalloutDismissed)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const calloutProps: ICalloutProps = {
|
||||||
|
calloutMaxWidth: 350,
|
||||||
|
ariaLabel: "New gallery",
|
||||||
|
role: "alertdialog",
|
||||||
|
gapSpace: 0,
|
||||||
|
target: ".galleryHeader",
|
||||||
|
directionalHint: DirectionalHint.leftTopEdge,
|
||||||
|
onDismiss: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
this.triggerRender();
|
||||||
|
},
|
||||||
|
setInitialFocus: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const openGalleryProps: ILinkProps = {
|
||||||
|
onClick: () => {
|
||||||
|
LocalStorageUtility.setEntryBoolean(StorageKey.GalleryCalloutDismissed, true);
|
||||||
|
this.container.openGallery();
|
||||||
|
this.triggerRender();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Callout {...calloutProps}>
|
||||||
|
<Stack tokens={{ childrenGap: 10, padding: 20 }}>
|
||||||
|
<Text variant="xLarge" block>
|
||||||
|
New gallery
|
||||||
|
</Text>
|
||||||
|
<Text block>
|
||||||
|
Sample notebooks are now combined in gallery. View and try out samples provided by Microsoft and other
|
||||||
|
contributors.
|
||||||
|
</Text>
|
||||||
|
<Link {...openGalleryProps}>Open gallery</Link>
|
||||||
|
</Stack>
|
||||||
|
</Callout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGalleryNotebooksTree(): TreeNode {
|
||||||
|
return {
|
||||||
|
label: "Gallery",
|
||||||
|
iconSrc: GalleryIcon,
|
||||||
|
className: "notebookHeader galleryHeader",
|
||||||
|
onClick: () => this.container.openGallery(),
|
||||||
|
isSelected: () => {
|
||||||
|
const activeTab = useTabs.getState().activeTab;
|
||||||
|
return activeTab && activeTab.tabKind === ViewModels.CollectionTabKind.Gallery;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildMyNotebooksTree(): TreeNode {
|
||||||
|
const myNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
||||||
|
this.myNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
this.container.openNotebook(item).then((hasOpened) => {
|
||||||
|
if (hasOpened) {
|
||||||
|
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
myNotebooksTree.isExpanded = true;
|
||||||
|
myNotebooksTree.isAlphaSorted = true;
|
||||||
|
// Remove "Delete" menu item from context menu
|
||||||
|
myNotebooksTree.contextMenu = myNotebooksTree.contextMenu.filter((menuItem) => menuItem.label !== "Delete");
|
||||||
|
return myNotebooksTree;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildGitHubNotebooksTree(): TreeNode {
|
||||||
|
const gitHubNotebooksTree: TreeNode = this.buildNotebookDirectoryNode(
|
||||||
|
this.gitHubNotebooksContentRoot,
|
||||||
|
(item: NotebookContentItem) => {
|
||||||
|
this.container.openNotebook(item).then((hasOpened) => {
|
||||||
|
if (hasOpened) {
|
||||||
|
mostRecentActivity.notebookWasItemOpened(userContext.databaseAccount?.id, item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
gitHubNotebooksTree.contextMenu = [
|
||||||
|
{
|
||||||
|
label: "Manage GitHub settings",
|
||||||
|
onClick: () =>
|
||||||
|
useSidePanel
|
||||||
|
.getState()
|
||||||
|
.openSidePanel(
|
||||||
|
"Manage GitHub settings",
|
||||||
|
<GitHubReposPanel
|
||||||
|
explorer={this.container}
|
||||||
|
gitHubClientProp={this.container.notebookManager.gitHubClient}
|
||||||
|
junoClientProp={this.container.notebookManager.junoClient}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Disconnect from GitHub",
|
||||||
|
onClick: () => {
|
||||||
|
TelemetryProcessor.trace(Action.NotebooksGitHubDisconnect, ActionModifiers.Mark, {
|
||||||
|
dataExplorerArea: Areas.Notebook,
|
||||||
|
});
|
||||||
|
this.container.notebookManager?.gitHubOAuthService.logout();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
gitHubNotebooksTree.isExpanded = true;
|
||||||
|
gitHubNotebooksTree.isAlphaSorted = true;
|
||||||
|
|
||||||
|
return gitHubNotebooksTree;
|
||||||
|
}
|
||||||
|
|
||||||
private buildChildNodes(
|
private buildChildNodes(
|
||||||
item: NotebookContentItem,
|
item: NotebookContentItem,
|
||||||
onFileClick: (item: NotebookContentItem) => void,
|
onFileClick: (item: NotebookContentItem) => void,
|
||||||
@@ -624,6 +800,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
iconSrc: NewNotebookIcon,
|
iconSrc: NewNotebookIcon,
|
||||||
onClick: () => this.container.onCreateDirectory(item),
|
onClick: () => this.container.onCreateDirectory(item),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Upload File",
|
||||||
|
iconSrc: NewNotebookIcon,
|
||||||
|
onClick: () => this.container.openUploadFilePanel(item),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
//disallow renaming of temporary notebook workspace
|
//disallow renaming of temporary notebook workspace
|
||||||
|
|||||||
@@ -17,17 +17,8 @@ export type KeyboardHandlerMap = Partial<Record<KeyboardAction, KeyboardActionHa
|
|||||||
* Each group can be updated separately, but, when updated, must be completely replaced.
|
* Each group can be updated separately, but, when updated, must be completely replaced.
|
||||||
*/
|
*/
|
||||||
export enum KeyboardActionGroup {
|
export enum KeyboardActionGroup {
|
||||||
/** Keyboard actions related to tab navigation. */
|
|
||||||
TABS = "TABS",
|
TABS = "TABS",
|
||||||
|
|
||||||
/** Keyboard actions managed by the global command bar. */
|
|
||||||
COMMAND_BAR = "COMMAND_BAR",
|
COMMAND_BAR = "COMMAND_BAR",
|
||||||
|
|
||||||
/**
|
|
||||||
* Keyboard actions specific to the active tab.
|
|
||||||
* This group is automatically cleared when the active tab changes.
|
|
||||||
*/
|
|
||||||
ACTIVE_TAB = "ACTIVE_TAB",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,8 +43,6 @@ export enum KeyboardAction {
|
|||||||
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
|
SELECT_LEFT_TAB = "SELECT_LEFT_TAB",
|
||||||
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
|
SELECT_RIGHT_TAB = "SELECT_RIGHT_TAB",
|
||||||
CLOSE_TAB = "CLOSE_TAB",
|
CLOSE_TAB = "CLOSE_TAB",
|
||||||
SEARCH = "SEARCH",
|
|
||||||
CLEAR_SEARCH = "CLEAR_SEARCH",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,8 +72,6 @@ const bindings: Record<KeyboardAction, string[]> = {
|
|||||||
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
|
[KeyboardAction.SELECT_LEFT_TAB]: ["$mod+Alt+[", "$mod+Shift+F6"],
|
||||||
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
|
[KeyboardAction.SELECT_RIGHT_TAB]: ["$mod+Alt+]", "$mod+F6"],
|
||||||
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
[KeyboardAction.CLOSE_TAB]: ["$mod+Alt+W"],
|
||||||
[KeyboardAction.SEARCH]: ["$mod+Shift+F"],
|
|
||||||
[KeyboardAction.CLEAR_SEARCH]: ["$mod+Shift+C"],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface KeyboardShortcutState {
|
interface KeyboardShortcutState {
|
||||||
@@ -104,24 +91,13 @@ interface KeyboardShortcutState {
|
|||||||
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
|
setHandlers: (group: KeyboardActionGroup, handlers: KeyboardHandlerMap) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KeyboardHandlerSetter = (handlers: KeyboardHandlerMap) => void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the calling component as the manager of the keyboard actions for the given group.
|
* Defines the calling component as the manager of the keyboard actions for the given group.
|
||||||
* @param group The group of keyboard actions to manage.
|
* @param group The group of keyboard actions to manage.
|
||||||
* @returns A function that can be used to set the keyboard action handlers for the given group.
|
* @returns A function that can be used to set the keyboard action handlers for the given group.
|
||||||
*/
|
*/
|
||||||
export const useKeyboardActionGroup: (group: KeyboardActionGroup) => KeyboardHandlerSetter =
|
export const useKeyboardActionGroup = (group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
|
||||||
(group: KeyboardActionGroup) => (handlers: KeyboardHandlerMap) =>
|
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
|
||||||
useKeyboardActionHandlers.getState().setHandlers(group, handlers);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the keyboard action handlers for the given group.
|
|
||||||
* @param group The group of keyboard actions to clear.
|
|
||||||
*/
|
|
||||||
export const clearKeyboardActionGroup = (group: KeyboardActionGroup) => {
|
|
||||||
useKeyboardActionHandlers.getState().setHandlers(group, {});
|
|
||||||
};
|
|
||||||
|
|
||||||
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
const useKeyboardActionHandlers: UseStore<KeyboardShortcutState> = create((set, get) => ({
|
||||||
allHandlers: {},
|
allHandlers: {},
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ const handleMessage = async (event: MessageEvent): Promise<void> => {
|
|||||||
|
|
||||||
updateConfigContext({
|
updateConfigContext({
|
||||||
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
ARM_ENDPOINT: normalizeArmEndpoint(inputs.csmEndpoint || configContext.ARM_ENDPOINT),
|
||||||
|
CATALOG_API_KEY: inputs.catalogAPIKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { armRequestWithoutPolling } from "../../Utils/arm/request";
|
import { get } from "../../Utils/arm/generatedClients/cosmos/locations";
|
||||||
|
import { armRequestWithoutPolling, getOfferingIdsRequest } from "../../Utils/arm/request";
|
||||||
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
import { selfServeTraceFailure, selfServeTraceStart, selfServeTraceSuccess } from "../SelfServeTelemetryProcessor";
|
||||||
import { RefreshResult } from "../SelfServeTypes";
|
import { RefreshResult } from "../SelfServeTypes";
|
||||||
import SqlX from "./SqlX";
|
import SqlX from "./SqlX";
|
||||||
import {
|
import {
|
||||||
FetchPricesResponse,
|
FetchPricesResponse,
|
||||||
|
GetOfferingIdsResponse,
|
||||||
|
OfferingIdMap,
|
||||||
|
OfferingIdRequest,
|
||||||
PriceMapAndCurrencyCode,
|
PriceMapAndCurrencyCode,
|
||||||
RegionItem,
|
RegionItem,
|
||||||
RegionsResponse,
|
RegionsResponse,
|
||||||
@@ -166,11 +170,21 @@ export const getRegions = async (): Promise<Array<RegionItem>> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getRegionShortName = async (regionDisplayName: string): Promise<string> => {
|
||||||
|
const locationsList = await get(userContext.subscriptionId, regionDisplayName);
|
||||||
|
|
||||||
|
if ("id" in locationsList) {
|
||||||
|
const locationId = locationsList.id;
|
||||||
|
return locationId.substring(locationId.lastIndexOf("/") + 1);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
const getFetchPricesPathForRegion = (subscriptionId: string): string => {
|
||||||
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
return `/subscriptions/${subscriptionId}/providers/Microsoft.CostManagement/fetchPrices`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Promise<PriceMapAndCurrencyCode> => {
|
export const getPriceMapAndCurrencyCode = async (map: OfferingIdMap): Promise<PriceMapAndCurrencyCode> => {
|
||||||
const telemetryData = {
|
const telemetryData = {
|
||||||
feature: "Calculate approximate cost",
|
feature: "Calculate approximate cost",
|
||||||
function: "getPriceMapAndCurrencyCode",
|
function: "getPriceMapAndCurrencyCode",
|
||||||
@@ -181,39 +195,94 @@ export const getPriceMapAndCurrencyCode = async (regions: Array<RegionItem>): Pr
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const priceMap = new Map<string, Map<string, number>>();
|
const priceMap = new Map<string, Map<string, number>>();
|
||||||
let currencyCode;
|
let billingCurrency;
|
||||||
for (const regionItem of regions) {
|
for (const region of map.keys()) {
|
||||||
const regionPriceMap = new Map<string, number>();
|
const regionPriceMap = new Map<string, number>();
|
||||||
|
const regionShortName = await getRegionShortName(region);
|
||||||
|
const requestBody: OfferingIdRequest = {
|
||||||
|
location: regionShortName,
|
||||||
|
ids: Array.from(map.get(region).keys()),
|
||||||
|
};
|
||||||
|
|
||||||
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
const response = await armRequestWithoutPolling<FetchPricesResponse>({
|
||||||
host: configContext.ARM_ENDPOINT,
|
host: configContext.ARM_ENDPOINT,
|
||||||
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
path: getFetchPricesPathForRegion(userContext.subscriptionId),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
apiVersion: "2020-01-01-preview",
|
apiVersion: "2023-04-01-preview",
|
||||||
queryParams: {
|
body: requestBody,
|
||||||
filter:
|
|
||||||
"armRegionName eq '" +
|
|
||||||
regionItem.locationName.split(" ").join("").toLowerCase() +
|
|
||||||
"' and serviceFamily eq 'Databases' and productName eq 'Azure Cosmos DB Dedicated Gateway - General Purpose'",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const item of response.result.Items) {
|
for (const item of response.result) {
|
||||||
if (currencyCode === undefined) {
|
if (item.error) {
|
||||||
currencyCode = item.currencyCode;
|
continue;
|
||||||
} else if (item.currencyCode !== currencyCode) {
|
}
|
||||||
|
|
||||||
|
if (billingCurrency === undefined) {
|
||||||
|
billingCurrency = item.billingCurrency;
|
||||||
|
} else if (item.billingCurrency !== billingCurrency) {
|
||||||
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
throw Error("Currency Code Mismatch: Currency code not same for all regions / skus.");
|
||||||
}
|
}
|
||||||
regionPriceMap.set(item.skuName, item.retailPrice);
|
|
||||||
|
const offeringId = item.id;
|
||||||
|
const skuName = map.get(region).get(offeringId);
|
||||||
|
const unitPriceinBillingCurrency = item.prices.find((x) => x.type === "Consumption")
|
||||||
|
?.unitPriceinBillingCurrency;
|
||||||
|
regionPriceMap.set(skuName, unitPriceinBillingCurrency);
|
||||||
}
|
}
|
||||||
priceMap.set(regionItem.locationName, regionPriceMap);
|
priceMap.set(region, regionPriceMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
selfServeTraceSuccess(telemetryData, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return { priceMap: priceMap, currencyCode: currencyCode };
|
return { priceMap: priceMap, billingCurrency: billingCurrency };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
selfServeTraceFailure(failureTelemetry, getPriceMapAndCurrencyCodeTimestamp);
|
||||||
return { priceMap: undefined, currencyCode: undefined };
|
return { priceMap: undefined, billingCurrency: undefined };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOfferingIdPathForRegion = (): string => {
|
||||||
|
return `/skus?serviceFamily=Databases&service=Azure Cosmos DB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOfferingIds = async (regions: Array<RegionItem>): Promise<OfferingIdMap> => {
|
||||||
|
const telemetryData = {
|
||||||
|
feature: "Get Offering Ids to calculate approximate cost",
|
||||||
|
function: "getOfferingIds",
|
||||||
|
description: "fetch offering ids API call",
|
||||||
|
selfServeClassName: SqlX.name,
|
||||||
|
};
|
||||||
|
const getOfferingIdsCodeTimestamp = selfServeTraceStart(telemetryData);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const offeringIdMap = new Map<string, Map<string, string>>();
|
||||||
|
for (const regionItem of regions) {
|
||||||
|
const regionOfferingIdMap = new Map<string, string>();
|
||||||
|
const regionShortName = await getRegionShortName(regionItem.locationName);
|
||||||
|
|
||||||
|
const response = await getOfferingIdsRequest<GetOfferingIdsResponse>({
|
||||||
|
host: configContext.CATALOG_ENDPOINT,
|
||||||
|
path: getOfferingIdPathForRegion(),
|
||||||
|
method: "GET",
|
||||||
|
apiVersion: "2023-05-01-preview",
|
||||||
|
queryParams: {
|
||||||
|
filter: "armRegionName eq '" + regionShortName + "'",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const item of response.result.items) {
|
||||||
|
if (item.offeringProperties?.length > 0) {
|
||||||
|
regionOfferingIdMap.set(item.offeringProperties[0].offeringId, item.skuName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offeringIdMap.set(regionItem.locationName, regionOfferingIdMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
selfServeTraceSuccess(telemetryData, getOfferingIdsCodeTimestamp);
|
||||||
|
return offeringIdMap;
|
||||||
|
} catch (err) {
|
||||||
|
const failureTelemetry = { err, selfServeClassName: SqlX.name };
|
||||||
|
selfServeTraceFailure(failureTelemetry, getOfferingIdsCodeTimestamp);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { BladeType, generateBladeLink } from "../SelfServeUtils";
|
|||||||
import {
|
import {
|
||||||
deleteDedicatedGatewayResource,
|
deleteDedicatedGatewayResource,
|
||||||
getCurrentProvisioningState,
|
getCurrentProvisioningState,
|
||||||
|
getOfferingIds,
|
||||||
getPriceMapAndCurrencyCode,
|
getPriceMapAndCurrencyCode,
|
||||||
getRegions,
|
getRegions,
|
||||||
refreshDedicatedGatewayProvisioning,
|
refreshDedicatedGatewayProvisioning,
|
||||||
@@ -370,9 +371,10 @@ export default class SqlX extends SelfServeBaseClass {
|
|||||||
});
|
});
|
||||||
|
|
||||||
regions = await getRegions();
|
regions = await getRegions();
|
||||||
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(regions);
|
const offeringIdMap = await getOfferingIds(regions);
|
||||||
|
const priceMapAndCurrencyCode = await getPriceMapAndCurrencyCode(offeringIdMap);
|
||||||
priceMap = priceMapAndCurrencyCode.priceMap;
|
priceMap = priceMapAndCurrencyCode.priceMap;
|
||||||
currencyCode = priceMapAndCurrencyCode.currencyCode;
|
currencyCode = priceMapAndCurrencyCode.billingCurrency;
|
||||||
|
|
||||||
const response = await getCurrentProvisioningState();
|
const response = await getCurrentProvisioningState();
|
||||||
if (response.status && response.status !== "Deleting") {
|
if (response.status && response.status !== "Deleting") {
|
||||||
|
|||||||
@@ -30,23 +30,51 @@ export type UpdateDedicatedGatewayRequestProperties = {
|
|||||||
serviceType: string;
|
serviceType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FetchPricesResponse = {
|
export type FetchPricesResponse = Array<PriceItem>;
|
||||||
Items: Array<PriceItem>;
|
|
||||||
NextPageLink: string | undefined;
|
export type PriceItem = {
|
||||||
Count: number;
|
prices: Array<PriceType>;
|
||||||
|
id: string;
|
||||||
|
billingCurrency: string;
|
||||||
|
error: PriceError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceType = {
|
||||||
|
type: string;
|
||||||
|
unitPriceinBillingCurrency: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceError = {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceMapAndCurrencyCode = {
|
export type PriceMapAndCurrencyCode = {
|
||||||
priceMap: Map<string, Map<string, number>>;
|
priceMap: Map<string, Map<string, number>>;
|
||||||
currencyCode: string;
|
billingCurrency: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PriceItem = {
|
export type GetOfferingIdsResponse = {
|
||||||
retailPrice: number;
|
items: Array<OfferingIdItem>;
|
||||||
skuName: string;
|
nextPageLink: string | undefined;
|
||||||
currencyCode: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OfferingIdItem = {
|
||||||
|
skuName: string;
|
||||||
|
offeringProperties: Array<OfferingProperties>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingProperties = {
|
||||||
|
offeringId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingIdRequest = {
|
||||||
|
ids: Array<string>;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OfferingIdMap = Map<string, Map<string, string>>;
|
||||||
|
|
||||||
export type RegionsResponse = {
|
export type RegionsResponse = {
|
||||||
properties: RegionsProperties;
|
properties: RegionsProperties;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -160,3 +160,52 @@ async function getOperationStatus(operationStatusUrl: string) {
|
|||||||
}
|
}
|
||||||
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
|
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getOfferingIdsRequest<T>({
|
||||||
|
host,
|
||||||
|
path,
|
||||||
|
apiVersion,
|
||||||
|
method,
|
||||||
|
body: requestBody,
|
||||||
|
queryParams,
|
||||||
|
}: Options): Promise<{ result: T; operationStatusUrl: string }> {
|
||||||
|
const url = new URL(path, host);
|
||||||
|
url.searchParams.append("api-version", configContext.armAPIVersion || apiVersion);
|
||||||
|
if (queryParams) {
|
||||||
|
queryParams.filter && url.searchParams.append("$filter", queryParams.filter);
|
||||||
|
queryParams.metricNames && url.searchParams.append("metricnames", queryParams.metricNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configContext.CATALOG_API_KEY) {
|
||||||
|
throw new Error("No catalog API key provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await window.fetch(url.href, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
[HttpHeaders.xAPIKey]: configContext.CATALOG_API_KEY,
|
||||||
|
},
|
||||||
|
body: requestBody ? JSON.stringify(requestBody) : undefined,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
let error: ARMError;
|
||||||
|
try {
|
||||||
|
const errorResponse = (await response.json()) as ParsedErrorResponse;
|
||||||
|
if ("error" in errorResponse) {
|
||||||
|
error = new ARMError(errorResponse.error.message);
|
||||||
|
error.code = errorResponse.error.code;
|
||||||
|
} else {
|
||||||
|
error = new ARMError(errorResponse.message);
|
||||||
|
error.code = errorResponse.code;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operationStatusUrl = (response.headers && response.headers.get("location")) || "";
|
||||||
|
const responseBody = (await response.json()) as T;
|
||||||
|
return { result: responseBody, operationStatusUrl: operationStatusUrl };
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user