mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-28 22:24:23 +00:00
Merge branch 'master' into replace-codemirror-with-monaco
This commit is contained in:
@@ -37,7 +37,7 @@ import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer
|
||||
import { BrowseQueriesPane } from "./Panes/BrowseQueriesPane";
|
||||
import { CassandraAPIDataClient, TableDataClient, TablesAPIDataClient } from "./Tables/TableDataClient";
|
||||
import { CommandBarComponentAdapter } from "./Menus/CommandBar/CommandBarComponentAdapter";
|
||||
import { configContext } from "../ConfigContext";
|
||||
import { configContext, updateConfigContext } from "../ConfigContext";
|
||||
import { ConsoleData, ConsoleDataType } from "./Menus/NotificationConsole/NotificationConsoleComponent";
|
||||
import { decryptJWTToken, getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
|
||||
@@ -975,6 +975,10 @@ export default class Explorer {
|
||||
this.sparkClusterConnectionInfo.valueHasMutated();
|
||||
}
|
||||
|
||||
if (this.isFeatureEnabled(Constants.Features.enableSDKoperations)) {
|
||||
updateUserContext({ useSDKOperations: true });
|
||||
}
|
||||
|
||||
featureSubcription.dispose();
|
||||
});
|
||||
|
||||
@@ -1475,38 +1479,33 @@ export default class Explorer {
|
||||
);
|
||||
};
|
||||
|
||||
if (this.isServerlessEnabled()) {
|
||||
// Serverless accounts don't support offers call
|
||||
refreshDatabases();
|
||||
} else {
|
||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
||||
this._setLoadingStatusText("Fetching offers...");
|
||||
offerPromise.then(
|
||||
(offers: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched offers.");
|
||||
refreshDatabases(offers);
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch offers.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
const offerPromise: Q.Promise<DataModels.Offer[]> = readOffers({ isServerless: this.isServerlessEnabled() });
|
||||
this._setLoadingStatusText("Fetching offers...");
|
||||
offerPromise.then(
|
||||
(offers: DataModels.Offer[]) => {
|
||||
this._setLoadingStatusText("Successfully fetched offers.");
|
||||
refreshDatabases(offers);
|
||||
},
|
||||
error => {
|
||||
this._setLoadingStatusText("Failed to fetch offers.");
|
||||
this.isRefreshingExplorer(false);
|
||||
deferred.reject(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.LoadDatabases,
|
||||
{
|
||||
databaseAccountName: this.databaseAccount().name,
|
||||
defaultExperience: this.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ResourceTree,
|
||||
error: JSON.stringify(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Error,
|
||||
`Error while refreshing databases: ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise.then(
|
||||
() => {
|
||||
@@ -1954,12 +1953,17 @@ export default class Explorer {
|
||||
|
||||
this._importExplorerConfigComplete = true;
|
||||
|
||||
updateConfigContext({
|
||||
ARM_ENDPOINT: this.armEndpoint()
|
||||
});
|
||||
|
||||
updateUserContext({
|
||||
authorizationToken,
|
||||
masterKey,
|
||||
databaseAccount
|
||||
databaseAccount,
|
||||
resourceGroup: inputs.resourceGroup,
|
||||
subscriptionId: inputs.subscriptionId
|
||||
});
|
||||
updateUserContext({ resourceGroup: inputs.resourceGroup, subscriptionId: inputs.subscriptionId });
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadDatabaseAccount,
|
||||
{
|
||||
|
||||
@@ -92,6 +92,11 @@ describe("getPkIdFromDocumentId", () => {
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[234, 'id']");
|
||||
});
|
||||
|
||||
it("should create pkid pair from partitioned graph (pk as boolean)", () => {
|
||||
const doc = createFakeDoc({ id: "id", mypk: true });
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("[true, 'id']");
|
||||
});
|
||||
|
||||
it("should create pkid pair from partitioned graph (pk as valid array value)", () => {
|
||||
const doc = createFakeDoc({ id: "id", mypk: [{ id: "someid", _value: "pkvalue" }] });
|
||||
expect(GraphExplorer.getPkIdFromDocumentId(doc, "mypk")).toEqual("['pkvalue', 'id']");
|
||||
|
||||
@@ -1371,7 +1371,7 @@ export class GraphExplorer extends React.Component<GraphExplorerProps, GraphExpl
|
||||
|
||||
if (collectionPartitionKeyProperty && d.hasOwnProperty(collectionPartitionKeyProperty)) {
|
||||
let pk = (d as any)[collectionPartitionKeyProperty];
|
||||
if (typeof pk !== "string" && typeof pk !== "number") {
|
||||
if (typeof pk !== "string" && typeof pk !== "number" && typeof pk !== "boolean") {
|
||||
if (Array.isArray(pk) && pk.length > 0) {
|
||||
// pk is [{ id: 'id', _value: 'value' }]
|
||||
pk = pk[0]["_value"];
|
||||
|
||||
@@ -14,8 +14,8 @@ import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstan
|
||||
import { AddDbUtilities } from "../../Shared/AddDatabaseUtility";
|
||||
import { CassandraAPIDataClient } from "../Tables/TableDataClient";
|
||||
import { ContextualPaneBase } from "./ContextualPaneBase";
|
||||
import { createDatabase } from "../../Common/dataAccess/createDatabase";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { refreshCachedOffers, refreshCachedResources, createDatabase } from "../../Common/DocumentClientUtilityBase";
|
||||
import { userContext } from "../../UserContext";
|
||||
|
||||
export default class AddDatabasePane extends ContextualPaneBase {
|
||||
@@ -304,76 +304,23 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
this.formErrors("");
|
||||
this.isExecuting(true);
|
||||
|
||||
const createDatabaseParameters: DataModels.RpParameters = {
|
||||
db: addDatabasePaneStartMessage.database.id,
|
||||
st: addDatabasePaneStartMessage.database.shared,
|
||||
offerThroughput: addDatabasePaneStartMessage.offerThroughput,
|
||||
sid: userContext.subscriptionId,
|
||||
rg: userContext.resourceGroup,
|
||||
dba: addDatabasePaneStartMessage.databaseAccountName
|
||||
const createDatabaseParams: DataModels.CreateDatabaseParams = {
|
||||
autoPilotMaxThroughput: this.maxAutoPilotThroughputSet(),
|
||||
databaseId: addDatabasePaneStartMessage.database.id,
|
||||
databaseLevelThroughput: addDatabasePaneStartMessage.database.shared,
|
||||
offerThroughput: addDatabasePaneStartMessage.offerThroughput
|
||||
};
|
||||
|
||||
const autopilotSettings = this._getAutopilotSettings();
|
||||
|
||||
if (this.container.isPreferredApiCassandra()) {
|
||||
this._createKeyspace(createDatabaseParameters, autopilotSettings, startKey);
|
||||
} else if (this.container.isPreferredApiMongoDB() && EnvironmentUtility.isAadUser()) {
|
||||
this._createMongoDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
||||
} else if (this.container.isPreferredApiGraph() && EnvironmentUtility.isAadUser()) {
|
||||
this._createGremlinDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
||||
} else if (this.container.isPreferredApiDocumentDB() && EnvironmentUtility.isAadUser()) {
|
||||
this._createSqlDatabase(createDatabaseParameters, autopilotSettings, startKey);
|
||||
} else {
|
||||
this._createDatabase(offerThroughput, startKey);
|
||||
}
|
||||
}
|
||||
|
||||
private _createSqlDatabase(
|
||||
createDatabaseParameters: DataModels.RpParameters,
|
||||
autoPilotSettings: DataModels.RpOptions,
|
||||
startKey: number
|
||||
) {
|
||||
AddDbUtilities.createSqlDatabase(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings).then(
|
||||
() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
createDatabase(createDatabaseParams).then(
|
||||
(database: DataModels.Database) => {
|
||||
this._onCreateDatabaseSuccess(offerThroughput, startKey);
|
||||
},
|
||||
(reason: any) => {
|
||||
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _createMongoDatabase(
|
||||
createDatabaseParameters: DataModels.RpParameters,
|
||||
autoPilotSettings: DataModels.RpOptions,
|
||||
startKey: number
|
||||
) {
|
||||
AddDbUtilities.createMongoDatabaseWithARM(
|
||||
this.container.armEndpoint(),
|
||||
createDatabaseParameters,
|
||||
autoPilotSettings
|
||||
).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _createGremlinDatabase(
|
||||
createDatabaseParameters: DataModels.RpParameters,
|
||||
autoPilotSettings: DataModels.RpOptions,
|
||||
startKey: number
|
||||
) {
|
||||
AddDbUtilities.createGremlinDatabase(
|
||||
this.container.armEndpoint(),
|
||||
createDatabaseParameters,
|
||||
autoPilotSettings
|
||||
).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createDatabaseParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public resetData() {
|
||||
this.databaseId("");
|
||||
this.databaseCreateNewShared(this.getSharedThroughputDefault());
|
||||
@@ -396,72 +343,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
private _createDatabase(offerThroughput: number, telemetryStartKey: number): void {
|
||||
const autoPilot: DataModels.AutoPilotCreationSettings = this._isAutoPilotSelectedAndWhatTier();
|
||||
const createRequest: DataModels.CreateDatabaseRequest = {
|
||||
databaseId: this.databaseId().trim(),
|
||||
offerThroughput,
|
||||
databaseLevelThroughput: this.databaseCreateNewShared(),
|
||||
autoPilot,
|
||||
hasAutoPilotV2FeatureFlag: this.hasAutoPilotV2FeatureFlag()
|
||||
};
|
||||
createDatabase(createRequest).then(
|
||||
(database: DataModels.Database) => {
|
||||
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
|
||||
},
|
||||
(reason: any) => {
|
||||
this._onCreateDatabaseFailure(reason, offerThroughput, reason);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _createKeyspace(
|
||||
createDatabaseParameters: DataModels.RpParameters,
|
||||
autoPilotSettings: DataModels.RpOptions,
|
||||
startKey: number
|
||||
): void {
|
||||
if (EnvironmentUtility.isAadUser()) {
|
||||
this._createKeyspaceUsingRP(this.container.armEndpoint(), createDatabaseParameters, autoPilotSettings, startKey);
|
||||
} else {
|
||||
this._createKeyspaceUsingProxy(createDatabaseParameters.offerThroughput, startKey);
|
||||
}
|
||||
}
|
||||
|
||||
private _createKeyspaceUsingProxy(offerThroughput: number, telemetryStartKey: number): void {
|
||||
const provisionThroughputQueryPart: string = this.databaseCreateNewShared()
|
||||
? `AND cosmosdb_provisioned_throughput=${offerThroughput}`
|
||||
: "";
|
||||
const createKeyspaceQuery: string = `CREATE KEYSPACE ${this.databaseId().trim()} WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } ${provisionThroughputQueryPart};`;
|
||||
(this.container.tableDataClient as CassandraAPIDataClient)
|
||||
.createKeyspace(
|
||||
this.container.databaseAccount().properties.cassandraEndpoint,
|
||||
this.container.databaseAccount().id,
|
||||
this.container,
|
||||
createKeyspaceQuery
|
||||
)
|
||||
.then(
|
||||
() => {
|
||||
this._onCreateDatabaseSuccess(offerThroughput, telemetryStartKey);
|
||||
},
|
||||
(reason: any) => {
|
||||
this._onCreateDatabaseFailure(reason, offerThroughput, telemetryStartKey);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private _createKeyspaceUsingRP(
|
||||
armEndpoint: string,
|
||||
createKeyspaceParameters: DataModels.RpParameters,
|
||||
autoPilotSettings: DataModels.RpOptions,
|
||||
startKey: number
|
||||
): void {
|
||||
AddDbUtilities.createCassandraKeyspace(armEndpoint, createKeyspaceParameters, autoPilotSettings).then(() => {
|
||||
Promise.all([refreshCachedOffers(), refreshCachedResources()]).then(() => {
|
||||
this._onCreateDatabaseSuccess(createKeyspaceParameters.offerThroughput, startKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _onCreateDatabaseSuccess(offerThroughput: number, startKey: number): void {
|
||||
this.isExecuting(false);
|
||||
this.close();
|
||||
@@ -582,20 +463,6 @@ export default class AddDatabasePane extends ContextualPaneBase {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getAutopilotSettings(): DataModels.RpOptions {
|
||||
if (
|
||||
(!this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.maxAutoPilotThroughputSet()) ||
|
||||
(this.hasAutoPilotV2FeatureFlag() && this.isAutoPilotSelected() && this.selectedAutoPilotTier())
|
||||
) {
|
||||
return !this.hasAutoPilotV2FeatureFlag()
|
||||
? {
|
||||
[Constants.HttpHeaders.autoPilotThroughput]: { maxThroughput: this.maxAutoPilotThroughputSet() * 1 }
|
||||
}
|
||||
: { [Constants.HttpHeaders.autoPilotTier]: this.selectedAutoPilotTier().toString() };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _updateThroughputLimitByDatabase() {
|
||||
const throughputDefaults = this.container.collectionCreationDefaults.throughput;
|
||||
this.throughput(throughputDefaults.shared);
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface GenericRightPaneProps {
|
||||
onSubmit: () => void;
|
||||
submitButtonText: string;
|
||||
title: string;
|
||||
isSubmitButtonVisible?: boolean;
|
||||
isSubmitButtonHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface GenericRightPaneState {
|
||||
@@ -108,7 +108,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
||||
<div className="paneFooter">
|
||||
<div className="leftpanel-okbut">
|
||||
<PrimaryButton
|
||||
style={{ visibility: this.props.isSubmitButtonVisible ? "visible" : "hidden" }}
|
||||
style={{ visibility: this.props.isSubmitButtonHidden ? "hidden" : "visible" }}
|
||||
ariaLabel="Submit"
|
||||
title="Submit"
|
||||
onClick={this.props.onSubmit}
|
||||
|
||||
@@ -52,7 +52,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||
submitButtonText: "Publish",
|
||||
onClose: () => this.close(),
|
||||
onSubmit: () => this.submit(),
|
||||
isSubmitButtonVisible: this.isCodeOfConductAccepted
|
||||
isSubmitButtonHidden: !this.isCodeOfConductAccepted
|
||||
};
|
||||
|
||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||
|
||||
@@ -285,7 +285,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||
<GalleryCardComponent
|
||||
data={{
|
||||
id: undefined,
|
||||
name: this.props.notebookName,
|
||||
name: this.state.notebookName,
|
||||
description: this.state.notebookDescription,
|
||||
gitSha: undefined,
|
||||
tags: this.state.notebookTags.split(","),
|
||||
|
||||
@@ -39,7 +39,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
||||
formErrorDetail: this.formErrorDetail,
|
||||
id: "uploaditemspane",
|
||||
isExecuting: this.isExecuting,
|
||||
isSubmitButtonVisible: true,
|
||||
title: "Upload Items",
|
||||
submitButtonText: "Upload",
|
||||
onClose: () => this.close(),
|
||||
|
||||
@@ -147,6 +147,30 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
const cellCodeType = "code";
|
||||
const cellMarkdownType = "markdown";
|
||||
const cellRawType = "raw";
|
||||
|
||||
const saveButtonChildren = [];
|
||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
saveButtonChildren.push({
|
||||
iconName: "Copy",
|
||||
onCommandClick: () => this.copyNotebook(),
|
||||
commandButtonLabel: copyToLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: copyToLabel
|
||||
});
|
||||
}
|
||||
|
||||
if (this.container.isGalleryPublishEnabled()) {
|
||||
saveButtonChildren.push({
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: async () => await this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel
|
||||
});
|
||||
}
|
||||
|
||||
let buttons: CommandButtonComponentProps[] = [
|
||||
{
|
||||
iconSrc: SaveIcon,
|
||||
@@ -156,34 +180,17 @@ export default class NotebookTabV2 extends TabsBase {
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel,
|
||||
children: this.container.isGalleryPublishEnabled()
|
||||
? [
|
||||
{
|
||||
iconName: "Save",
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel
|
||||
},
|
||||
{
|
||||
iconName: "Copy",
|
||||
onCommandClick: () => this.copyNotebook(),
|
||||
commandButtonLabel: copyToLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: copyToLabel
|
||||
},
|
||||
{
|
||||
iconName: "PublishContent",
|
||||
onCommandClick: async () => await this.publishToGallery(),
|
||||
commandButtonLabel: publishLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: publishLabel
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
children: saveButtonChildren.length && [
|
||||
{
|
||||
iconName: "Save",
|
||||
onCommandClick: () => this.notebookComponentAdapter.notebookSave(),
|
||||
commandButtonLabel: saveLabel,
|
||||
hasPopup: false,
|
||||
disabled: false,
|
||||
ariaLabel: saveLabel
|
||||
},
|
||||
...saveButtonChildren
|
||||
]
|
||||
},
|
||||
{
|
||||
iconSrc: null,
|
||||
|
||||
@@ -7,6 +7,7 @@ import Database from "../Tree/Database";
|
||||
import Explorer from "../Explorer";
|
||||
import SettingsTab from "../Tabs/SettingsTab";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { IndexingPolicies } from "../../Shared/Constants";
|
||||
|
||||
describe("Settings tab", () => {
|
||||
const baseCollection: DataModels.Collection = {
|
||||
@@ -16,7 +17,7 @@ describe("Settings tab", () => {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPath: "/_ts"
|
||||
},
|
||||
indexingPolicy: {},
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
@@ -51,7 +52,7 @@ describe("Settings tab", () => {
|
||||
defaultTtl: 200,
|
||||
partitionKey: null,
|
||||
conflictResolutionPolicy: null,
|
||||
indexingPolicy: {},
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
@@ -345,7 +346,6 @@ describe("Settings tab", () => {
|
||||
|
||||
const offer: DataModels.Offer = null;
|
||||
const defaultTtl = 200;
|
||||
const indexingPolicy = {};
|
||||
const database = new Database(explorer, baseDatabase, null);
|
||||
const conflictResolutionPolicy = {
|
||||
mode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
@@ -367,7 +367,7 @@ describe("Settings tab", () => {
|
||||
}
|
||||
: null,
|
||||
conflictResolutionPolicy: conflictResolutionPolicy,
|
||||
indexingPolicy: indexingPolicy,
|
||||
indexingPolicy: IndexingPolicies.SharedDatabaseDefault,
|
||||
_rid: "",
|
||||
_self: "",
|
||||
_etag: "",
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
||||
import { PlatformType } from "../../PlatformType";
|
||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||
import Explorer from "../Explorer";
|
||||
import { updateOffer, updateCollection } from "../../Common/DocumentClientUtilityBase";
|
||||
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||
import { updateCollection } from "../../Common/dataAccess/updateCollection";
|
||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||
@@ -1009,8 +1010,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
);
|
||||
}
|
||||
|
||||
public onSaveClick = (): Q.Promise<any> => {
|
||||
let promises: Q.Promise<void>[] = [];
|
||||
public onSaveClick = async (): Promise<any> => {
|
||||
this.isExecutionError(false);
|
||||
|
||||
this.isExecuting(true);
|
||||
@@ -1023,50 +1023,60 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
|
||||
const newCollectionAttributes: any = {};
|
||||
|
||||
if (this.shouldUpdateCollection()) {
|
||||
let defaultTtl: number;
|
||||
switch (this.timeToLive()) {
|
||||
case "on":
|
||||
defaultTtl = Number(this.timeToLiveSeconds());
|
||||
break;
|
||||
case "on-nodefault":
|
||||
defaultTtl = -1;
|
||||
break;
|
||||
case "off":
|
||||
default:
|
||||
defaultTtl = undefined;
|
||||
break;
|
||||
}
|
||||
try {
|
||||
if (this.shouldUpdateCollection()) {
|
||||
let defaultTtl: number;
|
||||
switch (this.timeToLive()) {
|
||||
case "on":
|
||||
defaultTtl = Number(this.timeToLiveSeconds());
|
||||
break;
|
||||
case "on-nodefault":
|
||||
defaultTtl = -1;
|
||||
break;
|
||||
case "off":
|
||||
default:
|
||||
defaultTtl = undefined;
|
||||
break;
|
||||
}
|
||||
|
||||
newCollectionAttributes.defaultTtl = defaultTtl;
|
||||
newCollectionAttributes.defaultTtl = defaultTtl;
|
||||
|
||||
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
|
||||
newCollectionAttributes.indexingPolicy = this.indexingPolicyContent();
|
||||
|
||||
newCollectionAttributes.changeFeedPolicy =
|
||||
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
|
||||
? ({
|
||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
||||
} as DataModels.ChangeFeedPolicy)
|
||||
newCollectionAttributes.changeFeedPolicy =
|
||||
this.changeFeedPolicyVisible() && this.changeFeedPolicyToggled() === ChangeFeedPolicyToggledState.On
|
||||
? ({
|
||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
||||
} as DataModels.ChangeFeedPolicy)
|
||||
: undefined;
|
||||
|
||||
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
|
||||
? this.analyticalStorageTtlSelection() === "on"
|
||||
? Number(this.analyticalStorageTtlSeconds())
|
||||
: Constants.AnalyticalStorageTtl.Infinite
|
||||
: undefined;
|
||||
|
||||
newCollectionAttributes.analyticalStorageTtl = this.isAnalyticalStorageEnabled
|
||||
? this.analyticalStorageTtlSelection() === "on"
|
||||
? Number(this.analyticalStorageTtlSeconds())
|
||||
: Constants.AnalyticalStorageTtl.Infinite
|
||||
: undefined;
|
||||
newCollectionAttributes.geospatialConfig = {
|
||||
type: this.geospatialConfigType()
|
||||
};
|
||||
|
||||
newCollectionAttributes.geospatialConfig = {
|
||||
type: this.geospatialConfigType()
|
||||
};
|
||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||
if (!!conflictResolutionChanges) {
|
||||
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
||||
}
|
||||
|
||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||
if (!!conflictResolutionChanges) {
|
||||
newCollectionAttributes.conflictResolutionPolicy = conflictResolutionChanges;
|
||||
}
|
||||
const newCollection: DataModels.Collection = _.extend(
|
||||
{},
|
||||
this.collection.rawDataModel,
|
||||
newCollectionAttributes
|
||||
);
|
||||
const updatedCollection: DataModels.Collection = await updateCollection(
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
newCollection
|
||||
);
|
||||
|
||||
const newCollection: DataModels.Collection = _.extend({}, this.collection.rawDataModel, newCollectionAttributes);
|
||||
const updateCollectionPromise = updateCollection(this.collection.databaseId, this.collection, newCollection).then(
|
||||
(updatedCollection: DataModels.Collection) => {
|
||||
if (updatedCollection) {
|
||||
this.collection.rawDataModel = updatedCollection;
|
||||
this.collection.defaultTtl(updatedCollection.defaultTtl);
|
||||
this.collection.analyticalStorageTtl(updatedCollection.analyticalStorageTtl);
|
||||
@@ -1076,164 +1086,133 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
||||
this.collection.changeFeedPolicy(updatedCollection.changeFeedPolicy);
|
||||
this.collection.geospatialConfig(updatedCollection.geospatialConfig);
|
||||
}
|
||||
);
|
||||
|
||||
promises.push(updateCollectionPromise);
|
||||
}
|
||||
|
||||
if (
|
||||
this.throughput.editableIsDirty() ||
|
||||
this.rupm.editableIsDirty() ||
|
||||
this._isAutoPilotDirty() ||
|
||||
this._hasProvisioningTypeChanged()
|
||||
) {
|
||||
const newThroughput = this.throughput();
|
||||
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
|
||||
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
|
||||
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
|
||||
} else {
|
||||
newOffer = _.extend({}, newOffer, {
|
||||
content: {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.isAutoPilotSelected()) {
|
||||
if (!this.hasAutoPilotV2FeatureFlag()) {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: this.autoPilotThroughput()
|
||||
};
|
||||
} else {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
tier: this.selectedAutoPilotTier()
|
||||
};
|
||||
}
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else {
|
||||
delete newOffer.content.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
this.isAutoPilotSelected(false);
|
||||
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
} else {
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.container != null
|
||||
this.throughput.editableIsDirty() ||
|
||||
this.rupm.editableIsDirty() ||
|
||||
this._isAutoPilotDirty() ||
|
||||
this._hasProvisioningTypeChanged()
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||
};
|
||||
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||
() => {
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(
|
||||
this.isAutoPilotSelected(),
|
||||
originalThroughputValue,
|
||||
this._getThroughputUnit(),
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
newThroughput
|
||||
)
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
},
|
||||
(error: any) => {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.collection && this.collection.databaseId,
|
||||
collectionName: this.collection && this.collection.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
);
|
||||
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||
} else {
|
||||
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
||||
(updatedOffer: DataModels.Offer) => {
|
||||
this.collection.offer(updatedOffer);
|
||||
this.collection.offer.valueHasMutated();
|
||||
}
|
||||
);
|
||||
const newThroughput = this.throughput();
|
||||
const isRUPerMinuteThroughputEnabled: boolean = this.rupm() === Constants.RUPMStates.on;
|
||||
let newOffer: DataModels.Offer = _.extend({}, this.collection.offer());
|
||||
const originalThroughputValue: number = this.throughput.getEditableOriginalValue();
|
||||
|
||||
promises.push(updateOfferPromise);
|
||||
}
|
||||
}
|
||||
|
||||
if (promises.length === 0) {
|
||||
this.isExecuting(false);
|
||||
}
|
||||
|
||||
return Q.all(promises)
|
||||
.then(
|
||||
() => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this._setBaseline();
|
||||
this.collection.readSettings();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
},
|
||||
(reason: any) => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
console.error(reason);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
if (newOffer.content) {
|
||||
newOffer.content.offerThroughput = newThroughput;
|
||||
newOffer.content.offerIsRUPerMinuteThroughputEnabled = isRUPerMinuteThroughputEnabled;
|
||||
} else {
|
||||
newOffer = _.extend({}, newOffer, {
|
||||
content: {
|
||||
offerThroughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||
}
|
||||
});
|
||||
}
|
||||
)
|
||||
.finally(() => this.isExecuting(false));
|
||||
|
||||
const headerOptions: RequestOptions = { initialHeaders: {} };
|
||||
|
||||
if (this.isAutoPilotSelected()) {
|
||||
if (!this.hasAutoPilotV2FeatureFlag()) {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
maxThroughput: this.autoPilotThroughput()
|
||||
};
|
||||
} else {
|
||||
newOffer.content.offerAutopilotSettings = {
|
||||
tier: this.selectedAutoPilotTier()
|
||||
};
|
||||
}
|
||||
|
||||
// user has changed from provisioned --> autoscale
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToAutopilot] = "true";
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
} else {
|
||||
delete newOffer.content.offerThroughput;
|
||||
}
|
||||
} else {
|
||||
this.isAutoPilotSelected(false);
|
||||
this.userCanChangeProvisioningTypes(false || !this.hasAutoPilotV2FeatureFlag());
|
||||
|
||||
// user has changed from autoscale --> provisioned
|
||||
if (!this.hasAutoPilotV2FeatureFlag() && this._hasProvisioningTypeChanged()) {
|
||||
headerOptions.initialHeaders[Constants.HttpHeaders.migrateOfferToManualThroughput] = "true";
|
||||
} else {
|
||||
delete newOffer.content.offerAutopilotSettings;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||
this.container != null
|
||||
) {
|
||||
const requestPayload = {
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
databaseAccountName: userContext.databaseAccount.name,
|
||||
resourceGroup: userContext.resourceGroup,
|
||||
databaseName: this.collection.databaseId,
|
||||
collectionName: this.collection.id(),
|
||||
throughput: newThroughput,
|
||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||
};
|
||||
|
||||
await updateOfferThroughputBeyondLimit(requestPayload);
|
||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||
this.throughput(originalThroughputValue);
|
||||
this.notificationStatusInfo(
|
||||
throughputApplyDelayedMessage(
|
||||
this.isAutoPilotSelected(),
|
||||
originalThroughputValue,
|
||||
this._getThroughputUnit(),
|
||||
this.collection.databaseId,
|
||||
this.collection.id(),
|
||||
newThroughput
|
||||
)
|
||||
);
|
||||
this.throughput.valueHasMutated(); // force component re-render
|
||||
} else {
|
||||
const updatedOffer: DataModels.Offer = await updateOffer(this.collection.offer(), newOffer, headerOptions);
|
||||
this.collection.offer(updatedOffer);
|
||||
this.collection.offer.valueHasMutated();
|
||||
}
|
||||
}
|
||||
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this._setBaseline();
|
||||
this.collection.readSettings();
|
||||
this._wasAutopilotOriginallySet(this.isAutoPilotSelected());
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
this.isExecutionError(true);
|
||||
console.error(error);
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.UpdateSettings,
|
||||
{
|
||||
databaseAccountName: this.container.databaseAccount().name,
|
||||
databaseName: this.collection && this.collection.databaseId,
|
||||
collectionName: this.collection && this.collection.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.tabTitle(),
|
||||
error: error
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
|
||||
this.isExecuting(false);
|
||||
};
|
||||
|
||||
public onRevertClick = (): Q.Promise<any> => {
|
||||
|
||||
@@ -648,7 +648,9 @@ export default class Collection implements ViewModels.Collection {
|
||||
});
|
||||
// TODO: Use the collection entity cache to get quota info
|
||||
const quotaInfoPromise: Q.Promise<DataModels.CollectionQuotaInfo> = readCollectionQuotaInfo(this);
|
||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
|
||||
isServerless: this.container.isServerlessEnabled()
|
||||
});
|
||||
Q.all([quotaInfoPromise, offerInfoPromise]).then(
|
||||
() => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
@@ -657,9 +659,7 @@ export default class Collection implements ViewModels.Collection {
|
||||
const quotaInfo = _.omit(quotaInfoWithUniqueKeyPolicy, "uniqueKeyPolicy");
|
||||
|
||||
const collectionOffer = this._getOfferForCollection(offerInfoPromise.valueOf(), collectionDataModel);
|
||||
const isDatabaseShared = this.getDatabase() && this.getDatabase().isDatabaseShared();
|
||||
const isServerless = this.container.isServerlessEnabled();
|
||||
if ((isDatabaseShared || isServerless) && !collectionOffer) {
|
||||
if (!collectionOffer) {
|
||||
this.quotaInfo(quotaInfo);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.LoadOffers,
|
||||
|
||||
@@ -123,10 +123,6 @@ export default class Database implements ViewModels.Database {
|
||||
|
||||
public readSettings(): Q.Promise<void> {
|
||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
||||
if (this.container.isServerlessEnabled()) {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
this.container.isRefreshingExplorer(true);
|
||||
const databaseDataModel: DataModels.Database = <DataModels.Database>{
|
||||
id: this.id(),
|
||||
@@ -138,7 +134,9 @@ export default class Database implements ViewModels.Database {
|
||||
defaultExperience: this.container.defaultExperience()
|
||||
});
|
||||
|
||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers();
|
||||
const offerInfoPromise: Q.Promise<DataModels.Offer[]> = readOffers({
|
||||
isServerless: this.container.isServerlessEnabled()
|
||||
});
|
||||
Q.all([offerInfoPromise]).then(
|
||||
() => {
|
||||
this.container.isRefreshingExplorer(false);
|
||||
@@ -147,6 +145,11 @@ export default class Database implements ViewModels.Database {
|
||||
offerInfoPromise.valueOf(),
|
||||
databaseDataModel
|
||||
);
|
||||
|
||||
if (!databaseOffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
readOffer(databaseOffer).then((offerDetail: DataModels.OfferWithHeaders) => {
|
||||
const offerThroughputInfo: DataModels.OfferThroughputInfo = {
|
||||
minimumRUForCollection:
|
||||
|
||||
@@ -546,43 +546,52 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||
(activeTab as any).notebookPath() === item.path
|
||||
);
|
||||
},
|
||||
contextMenu: createFileContextMenu
|
||||
? [
|
||||
{
|
||||
label: "Rename",
|
||||
iconSrc: NotebookIcon,
|
||||
onClick: () => this.container.renameNotebook(item)
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
iconSrc: DeleteIcon,
|
||||
onClick: () => {
|
||||
this.container.showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
`Are you sure you want to delete "${item.name}"`,
|
||||
"Delete",
|
||||
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Copy to ...",
|
||||
iconSrc: CopyIcon,
|
||||
onClick: () => this.copyNotebook(item)
|
||||
},
|
||||
{
|
||||
label: "Download",
|
||||
iconSrc: NotebookIcon,
|
||||
onClick: () => this.container.downloadFile(item)
|
||||
}
|
||||
]
|
||||
: undefined,
|
||||
contextMenu: createFileContextMenu && this.createFileContextMenu(item),
|
||||
data: item
|
||||
};
|
||||
}
|
||||
|
||||
private createFileContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
||||
let items: TreeNodeMenuItem[] = [
|
||||
{
|
||||
label: "Rename",
|
||||
iconSrc: NotebookIcon,
|
||||
onClick: () => this.container.renameNotebook(item)
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
iconSrc: DeleteIcon,
|
||||
onClick: () => {
|
||||
this.container.showOkCancelModalDialog(
|
||||
"Confirm delete",
|
||||
`Are you sure you want to delete "${item.name}"`,
|
||||
"Delete",
|
||||
() => this.container.deleteNotebookFile(item).then(() => this.triggerRender()),
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "Copy to ...",
|
||||
iconSrc: CopyIcon,
|
||||
onClick: () => this.copyNotebook(item)
|
||||
},
|
||||
{
|
||||
label: "Download",
|
||||
iconSrc: NotebookIcon,
|
||||
onClick: () => this.container.downloadFile(item)
|
||||
}
|
||||
];
|
||||
|
||||
// "Copy to ..." isn't needed if github locations are not available
|
||||
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||
items = items.filter(item => item.label !== "Copy to ...");
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private copyNotebook = async (item: NotebookContentItem) => {
|
||||
const content = await this.container.readFile(item);
|
||||
if (content) {
|
||||
|
||||
Reference in New Issue
Block a user