mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-25 21:47:52 +00:00
merge
This commit is contained in:
commit
a8eeeebb59
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -134,6 +134,11 @@ jobs:
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_SQL }}
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
name: videos
|
||||||
|
if: ${{ failure() }}
|
||||||
|
with:
|
||||||
|
path: "**/*.mp4"
|
||||||
endtoendmongo:
|
endtoendmongo:
|
||||||
name: "End To End Tests | Mongo"
|
name: "End To End Tests | Mongo"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
@ -163,6 +168,11 @@ jobs:
|
|||||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
if: ${{ failure() }}
|
||||||
|
name: videos
|
||||||
|
with:
|
||||||
|
path: "**/*.mp4"
|
||||||
accessibility:
|
accessibility:
|
||||||
name: "Accessibility | Hosted"
|
name: "Accessibility | Hosted"
|
||||||
needs: [lint, format, compile, unittest]
|
needs: [lint, format, compile, unittest]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"pluginsFile": false,
|
"pluginsFile": false,
|
||||||
"fixturesFolder": false,
|
"fixturesFolder": false,
|
||||||
"supportFile": "./support/index.js",
|
"supportFile": "./support/index.js",
|
||||||
"defaultCommandTimeout": 60000,
|
"defaultCommandTimeout": 90000,
|
||||||
"chromeWebSecurity": false,
|
"chromeWebSecurity": false,
|
||||||
"reporter": "mochawesome",
|
"reporter": "mochawesome",
|
||||||
"reporterOptions": {
|
"reporterOptions": {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "cypress run",
|
"test": "cypress run",
|
||||||
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
"wait-for-server": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/",
|
||||||
"test:sql": "cypress run --browser chrome --headless --spec \"./integration/dataexplorer/SQL/*\"",
|
"test:sql": "cypress run --browser chrome --spec \"./integration/dataexplorer/SQL/*\"",
|
||||||
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
"test:ci": "wait-on -t 240000 -i 5000 -v https-get://0.0.0.0:1234/ https-get://0.0.0.0:8081/_explorer/index.html && cypress run --browser chrome --headless",
|
||||||
"test:debug": "cypress open"
|
"test:debug": "cypress open"
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,12 @@ export class AuthorizationEndpoints {
|
|||||||
public static common: string = "https://login.windows.net/";
|
public static common: string = "https://login.windows.net/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CodeOfConductEndpoints {
|
||||||
|
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||||
|
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||||
|
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||||
|
}
|
||||||
|
|
||||||
export class BackendEndpoints {
|
export class BackendEndpoints {
|
||||||
public static localhost: string = "https://localhost:12900";
|
public static localhost: string = "https://localhost:12900";
|
||||||
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
||||||
@ -14,7 +20,10 @@ export class BackendEndpoints {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class EndpointsRegex {
|
export class EndpointsRegex {
|
||||||
public static readonly cassandra = "AccountEndpoint=(.*).cassandra.cosmosdb.azure.com";
|
public static readonly cassandra = [
|
||||||
|
"AccountEndpoint=(.*).cassandra.cosmosdb.azure.com",
|
||||||
|
"HostName=(.*).cassandra.cosmos.azure.com"
|
||||||
|
];
|
||||||
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
public static readonly mongo = "mongodb://.*:(.*)@(.*).documents.azure.com";
|
||||||
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
public static readonly mongoCompute = "mongodb://.*:(.*)@(.*).mongo.cosmos.azure.com";
|
||||||
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
public static readonly sql = "AccountEndpoint=https://(.*).documents.azure.com";
|
||||||
@ -113,6 +122,8 @@ export class Features {
|
|||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
|
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||||
|
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
public static readonly notebookServerUrl = "notebookserverurl";
|
public static readonly notebookServerUrl = "notebookserverurl";
|
||||||
|
@ -26,7 +26,6 @@ import { OfferUtils } from "../Utils/OfferUtils";
|
|||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
import StoredProcedure from "../Explorer/Tree/StoredProcedure";
|
||||||
import { Platform, configContext } from "../ConfigContext";
|
import { Platform, configContext } from "../ConfigContext";
|
||||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
|
||||||
import DocumentId from "../Explorer/Tree/DocumentId";
|
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||||
import ConflictId from "../Explorer/Tree/ConflictId";
|
import ConflictId from "../Explorer/Tree/ConflictId";
|
||||||
|
|
||||||
@ -419,26 +418,6 @@ export function deleteTrigger(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readCollections(database: ViewModels.Database, options: any): Q.Promise<DataModels.Collection[]> {
|
|
||||||
return Q(
|
|
||||||
client()
|
|
||||||
.database(database.id())
|
|
||||||
.containers.readAll()
|
|
||||||
.fetchAll()
|
|
||||||
.then(response => response.resources as DataModels.Collection[])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
|
|
||||||
return Q(
|
|
||||||
client()
|
|
||||||
.database(databaseId)
|
|
||||||
.container(collectionId)
|
|
||||||
.read()
|
|
||||||
.then(response => response.resource as DataModels.Collection)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollectionQuotaInfo(
|
export function readCollectionQuotaInfo(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
options: any
|
options: any
|
||||||
@ -508,26 +487,6 @@ export function readOffer(requestedResource: DataModels.Offer, options: any): Q.
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDatabases(options: any): Q.Promise<DataModels.Database[]> {
|
|
||||||
try {
|
|
||||||
if (configContext.platform === Platform.Portal) {
|
|
||||||
return sendCachedDataMessage<DataModels.Database[]>(MessageTypes.AllDatabases, [
|
|
||||||
(<any>window).dataExplorer.databaseAccount().id,
|
|
||||||
Constants.ClientDefaults.portalCacheTimeoutMs
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If error getting cached DBs, continue on and read via SDK
|
|
||||||
}
|
|
||||||
|
|
||||||
return Q(
|
|
||||||
client()
|
|
||||||
.databases.readAll()
|
|
||||||
.fetchAll()
|
|
||||||
.then(response => response.resources as DataModels.Database[])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOrCreateDatabaseAndCollection(
|
export function getOrCreateDatabaseAndCollection(
|
||||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||||
options: any
|
options: any
|
||||||
@ -640,29 +599,6 @@ export function queryConflicts(
|
|||||||
return Q(documentsIterator);
|
return Q(documentsIterator);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateOfferThroughputBeyondLimit(
|
|
||||||
request: DataModels.UpdateOfferThroughputRequest
|
|
||||||
): Promise<void> {
|
|
||||||
if (configContext.platform !== Platform.Portal) {
|
|
||||||
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
|
||||||
}
|
|
||||||
|
|
||||||
const explorer = window.dataExplorer;
|
|
||||||
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(request),
|
|
||||||
headers: { [authorizationHeader.header]: authorizationHeader.token }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
throw new Error(await response.text());
|
|
||||||
}
|
|
||||||
|
|
||||||
function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
|
function _createDatabase(request: DataModels.CreateDatabaseRequest, options: any = {}): Q.Promise<DataModels.Database> {
|
||||||
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
|
const { databaseId, databaseLevelThroughput, offerThroughput, autoPilot, hasAutoPilotV2FeatureFlag } = request;
|
||||||
const createBody: DatabaseRequest = { id: databaseId };
|
const createBody: DatabaseRequest = { id: databaseId };
|
||||||
|
@ -383,40 +383,6 @@ export function updateOffer(
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateOfferThroughputBeyondLimit(
|
|
||||||
requestPayload: DataModels.UpdateOfferThroughputRequest
|
|
||||||
): Q.Promise<void> {
|
|
||||||
const deferred: Q.Deferred<void> = Q.defer<void>();
|
|
||||||
const resourceDescriptionInfo: string = requestPayload.collectionName
|
|
||||||
? `database ${requestPayload.databaseName} and container ${requestPayload.collectionName}`
|
|
||||||
: `database ${requestPayload.databaseName}`;
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Requesting increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
DataAccessUtilityBase.updateOfferThroughputBeyondLimit(requestPayload)
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Info,
|
|
||||||
`Successfully requested an increase in throughput to ${requestPayload.throughput} for ${resourceDescriptionInfo}`
|
|
||||||
);
|
|
||||||
deferred.resolve();
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Failed to request an increase in throughput for ${requestPayload.throughput}: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => NotificationConsoleUtils.clearInProgressMessageWithId(id));
|
|
||||||
|
|
||||||
return deferred.promise.timeout(Constants.ClientDefaults.requestTimeoutMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateStoredProcedure(
|
export function updateStoredProcedure(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
storedProcedure: DataModels.StoredProcedure,
|
storedProcedure: DataModels.StoredProcedure,
|
||||||
@ -840,63 +806,6 @@ export function refreshCachedOffers(): Q.Promise<void> {
|
|||||||
return DataAccessUtilityBase.refreshCachedOffers();
|
return DataAccessUtilityBase.refreshCachedOffers();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readCollections(database: ViewModels.Database, options: any = {}): Q.Promise<DataModels.Collection[]> {
|
|
||||||
var deferred = Q.defer<DataModels.Collection[]>();
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Querying containers for database ${database.id()}`
|
|
||||||
);
|
|
||||||
DataAccessUtilityBase.readCollections(database, options)
|
|
||||||
.then(
|
|
||||||
(collections: DataModels.Collection[]) => {
|
|
||||||
deferred.resolve(collections);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while querying containers for database ${database.id()}:\n ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollection(databaseId: string, collectionId: string): Q.Promise<DataModels.Collection> {
|
|
||||||
const deferred = Q.defer<DataModels.Collection>();
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.InProgress,
|
|
||||||
`Querying container ${collectionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
DataAccessUtilityBase.readCollection(databaseId, collectionId)
|
|
||||||
.then(
|
|
||||||
(collection: DataModels.Collection) => {
|
|
||||||
deferred.resolve(collection);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
Logger.logError(JSON.stringify(error), "ReadCollections", error.code);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readCollectionQuotaInfo(
|
export function readCollectionQuotaInfo(
|
||||||
collection: ViewModels.Collection,
|
collection: ViewModels.Collection,
|
||||||
options?: any
|
options?: any
|
||||||
@ -984,31 +893,6 @@ export function readOffer(
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readDatabases(options: any): Q.Promise<DataModels.Database[]> {
|
|
||||||
var deferred = Q.defer<any>();
|
|
||||||
const id = NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.InProgress, "Querying databases");
|
|
||||||
DataAccessUtilityBase.readDatabases(options)
|
|
||||||
.then(
|
|
||||||
(databases: DataModels.Database[]) => {
|
|
||||||
deferred.resolve(databases);
|
|
||||||
},
|
|
||||||
(error: any) => {
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(
|
|
||||||
ConsoleDataType.Error,
|
|
||||||
`Error while querying databases:\n ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
Logger.logError(JSON.stringify(error), "ReadDatabases", error.code);
|
|
||||||
sendNotificationForError(error);
|
|
||||||
deferred.reject(error);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.finally(() => {
|
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOrCreateDatabaseAndCollection(
|
export function getOrCreateDatabaseAndCollection(
|
||||||
request: DataModels.CreateDatabaseAndCollectionRequest,
|
request: DataModels.CreateDatabaseAndCollectionRequest,
|
||||||
options: any = {}
|
options: any = {}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
export function replaceKnownError(err: string): string {
|
export function replaceKnownError(err: string): string {
|
||||||
|
@ -1,23 +1,46 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../MessageHandler");
|
jest.mock("../MessageHandler");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
import { deleteCollection } from "./deleteCollection";
|
import { deleteCollection } from "./deleteCollection";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
import { sendCachedDataMessage } from "../MessageHandler";
|
import { sendCachedDataMessage } from "../MessageHandler";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
|
||||||
describe("deleteCollection", () => {
|
describe("deleteCollection", () => {
|
||||||
it("should call ARM if logged in with AAD", async () => {
|
beforeAll(() => {
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
name: "test"
|
name: "test"
|
||||||
} as DatabaseAccount
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
});
|
});
|
||||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
await deleteCollection("database", "collection");
|
await deleteCollection("database", "collection");
|
||||||
expect(armRequest).toHaveBeenCalled();
|
expect(armRequest).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
// TODO: Test non-AAD case
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
container: () => {
|
||||||
|
return {
|
||||||
|
delete: (): unknown => undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await deleteCollection("database", "collection");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { deleteSqlContainer } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { deleteCassandraTable } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { deleteMongoDBCollection } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { deleteGremlinGraph } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { deleteTable } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
import { refreshCachedResources } from "../DataAccessUtilityBase";
|
||||||
@ -9,13 +16,7 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
|||||||
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
const clearMessage = logConsoleProgress(`Deleting container ${collectionId}`);
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (window.authType === AuthType.AAD) {
|
||||||
await deleteSqlContainer(
|
await deleteCollectionWithARM(databaseId, collectionId);
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
databaseId,
|
|
||||||
collectionId
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await client()
|
await client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
@ -24,9 +25,33 @@ export async function deleteCollection(databaseId: string, collectionId: string)
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
|
logConsoleError(`Error while deleting container ${collectionId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "DeleteCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
logConsoleInfo(`Successfully deleted container ${collectionId}`);
|
||||||
clearMessage();
|
clearMessage();
|
||||||
await refreshCachedResources();
|
await refreshCachedResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteCollectionWithARM(databaseId: string, collectionId: string): Promise<void> {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return deleteSqlContainer(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return deleteMongoDBCollection(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return deleteCassandraTable(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return deleteGremlinGraph(subscriptionId, resourceGroup, accountName, databaseId, collectionId);
|
||||||
|
case DefaultAccountExperienceType.Table:
|
||||||
|
return deleteTable(subscriptionId, resourceGroup, accountName, collectionId);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,23 +1,42 @@
|
|||||||
jest.mock("../../Utils/arm/request");
|
jest.mock("../../Utils/arm/request");
|
||||||
jest.mock("../MessageHandler");
|
jest.mock("../MessageHandler");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
import { deleteDatabase } from "./deleteDatabase";
|
import { deleteDatabase } from "./deleteDatabase";
|
||||||
import { armRequest } from "../../Utils/arm/request";
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
import { updateUserContext } from "../../UserContext";
|
import { updateUserContext } from "../../UserContext";
|
||||||
import { DatabaseAccount } from "../../Contracts/DataModels";
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
import { sendCachedDataMessage } from "../MessageHandler";
|
import { sendCachedDataMessage } from "../MessageHandler";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
|
||||||
describe("deleteDatabase", () => {
|
describe("deleteDatabase", () => {
|
||||||
it("should call ARM if logged in with AAD", async () => {
|
beforeAll(() => {
|
||||||
window.authType = AuthType.AAD;
|
|
||||||
updateUserContext({
|
updateUserContext({
|
||||||
databaseAccount: {
|
databaseAccount: {
|
||||||
name: "test"
|
name: "test"
|
||||||
} as DatabaseAccount
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
});
|
});
|
||||||
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
(sendCachedDataMessage as jest.Mock).mockResolvedValue(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
await deleteDatabase("database");
|
await deleteDatabase("database");
|
||||||
expect(armRequest).toHaveBeenCalled();
|
expect(armRequest).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
// TODO: Test non-AAD case
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
delete: (): unknown => undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await deleteDatabase("database");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { AuthType } from "../../AuthType";
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
import { deleteSqlDatabase } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { deleteCassandraKeyspace } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { deleteMongoDBDatabase } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { deleteGremlinDatabase } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
import { logConsoleError, logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
import { client } from "../CosmosClient";
|
import { client } from "../CosmosClient";
|
||||||
@ -12,12 +16,7 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (window.authType === AuthType.AAD) {
|
if (window.authType === AuthType.AAD) {
|
||||||
await deleteSqlDatabase(
|
await deleteDatabaseWithARM(databaseId);
|
||||||
userContext.subscriptionId,
|
|
||||||
userContext.resourceGroup,
|
|
||||||
userContext.databaseAccount.name,
|
|
||||||
databaseId
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await client()
|
await client()
|
||||||
.database(databaseId)
|
.database(databaseId)
|
||||||
@ -33,3 +32,23 @@ export async function deleteDatabase(databaseId: string): Promise<void> {
|
|||||||
clearMessage();
|
clearMessage();
|
||||||
await refreshCachedResources();
|
await refreshCachedResources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteDatabaseWithARM(databaseId: string): Promise<void> {
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
return deleteSqlDatabase(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
return deleteMongoDBDatabase(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
return deleteCassandraKeyspace(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
return deleteGremlinDatabase(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
35
src/Common/dataAccess/readCollection.test.ts
Normal file
35
src/Common/dataAccess/readCollection.test.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readCollection } from "./readCollection";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readCollection", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if logged in with resource token", async () => {
|
||||||
|
window.authType = AuthType.ResourceToken;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
container: () => {
|
||||||
|
return {
|
||||||
|
read: (): unknown => ({})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readCollection("database", "collection");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
24
src/Common/dataAccess/readCollection.ts
Normal file
24
src/Common/dataAccess/readCollection.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
|
||||||
|
export async function readCollection(databaseId: string, collectionId: string): Promise<DataModels.Collection> {
|
||||||
|
let collection: DataModels.Collection;
|
||||||
|
const clearMessage = logConsoleProgress(`Querying container ${collectionId}`);
|
||||||
|
try {
|
||||||
|
const response = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.container(collectionId)
|
||||||
|
.read();
|
||||||
|
collection = response.resource as DataModels.Collection;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying container ${collectionId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadCollection", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return collection;
|
||||||
|
}
|
45
src/Common/dataAccess/readCollections.test.ts
Normal file
45
src/Common/dataAccess/readCollections.test.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
jest.mock("../../Utils/arm/request");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readCollections } from "./readCollections";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readCollections", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
await readCollections("database");
|
||||||
|
expect(armRequest).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
database: () => {
|
||||||
|
return {
|
||||||
|
containers: {
|
||||||
|
readAll: () => {
|
||||||
|
return {
|
||||||
|
fetchAll: (): unknown => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readCollections("database");
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
66
src/Common/dataAccess/readCollections.ts
Normal file
66
src/Common/dataAccess/readCollections.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { listSqlContainers } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { listCassandraTables } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { listMongoDBCollections } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { listGremlinGraphs } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { listTables } from "../../Utils/arm/generatedClients/2020-04-01/tableResources";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
|
let collections: DataModels.Collection[];
|
||||||
|
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
|
||||||
|
try {
|
||||||
|
if (window.authType === AuthType.AAD) {
|
||||||
|
collections = await readCollectionsWithARM(databaseId);
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.database(databaseId)
|
||||||
|
.containers.readAll()
|
||||||
|
.fetchAll();
|
||||||
|
collections = sdkResponse.resources as DataModels.Collection[];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying containers for database ${databaseId}:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadCollections", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readCollectionsWithARM(databaseId: string): Promise<DataModels.Collection[]> {
|
||||||
|
let rpResponse;
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
rpResponse = await listSqlContainers(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
rpResponse = await listMongoDBCollections(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
rpResponse = await listCassandraTables(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
rpResponse = await listGremlinGraphs(subscriptionId, resourceGroup, accountName, databaseId);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Table:
|
||||||
|
rpResponse = await listTables(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpResponse?.value?.map(collection => collection.properties?.resource as DataModels.Collection);
|
||||||
|
}
|
41
src/Common/dataAccess/readDatabases.test.ts
Normal file
41
src/Common/dataAccess/readDatabases.test.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
jest.mock("../../Utils/arm/request");
|
||||||
|
jest.mock("../CosmosClient");
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DatabaseAccount } from "../../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { armRequest } from "../../Utils/arm/request";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { readDatabases } from "./readDatabases";
|
||||||
|
import { updateUserContext } from "../../UserContext";
|
||||||
|
|
||||||
|
describe("readDatabases", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
updateUserContext({
|
||||||
|
databaseAccount: {
|
||||||
|
name: "test"
|
||||||
|
} as DatabaseAccount,
|
||||||
|
defaultExperience: DefaultAccountExperienceType.DocumentDB
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call ARM if logged in with AAD", async () => {
|
||||||
|
window.authType = AuthType.AAD;
|
||||||
|
await readDatabases();
|
||||||
|
expect(armRequest).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call SDK if not logged in with non-AAD method", async () => {
|
||||||
|
window.authType = AuthType.MasterKey;
|
||||||
|
(client as jest.Mock).mockReturnValue({
|
||||||
|
databases: {
|
||||||
|
readAll: () => {
|
||||||
|
return {
|
||||||
|
fetchAll: (): unknown => []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await readDatabases();
|
||||||
|
expect(client).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
61
src/Common/dataAccess/readDatabases.ts
Normal file
61
src/Common/dataAccess/readDatabases.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import * as DataModels from "../../Contracts/DataModels";
|
||||||
|
import { AuthType } from "../../AuthType";
|
||||||
|
import { DefaultAccountExperienceType } from "../../DefaultAccountExperienceType";
|
||||||
|
import { client } from "../CosmosClient";
|
||||||
|
import { listSqlDatabases } from "../../Utils/arm/generatedClients/2020-04-01/sqlResources";
|
||||||
|
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/2020-04-01/cassandraResources";
|
||||||
|
import { listMongoDBDatabases } from "../../Utils/arm/generatedClients/2020-04-01/mongoDBResources";
|
||||||
|
import { listGremlinDatabases } from "../../Utils/arm/generatedClients/2020-04-01/gremlinResources";
|
||||||
|
import { logConsoleProgress, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { logError } from "../Logger";
|
||||||
|
import { sendNotificationForError } from "./sendNotificationForError";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
|
export async function readDatabases(): Promise<DataModels.Database[]> {
|
||||||
|
let databases: DataModels.Database[];
|
||||||
|
const clearMessage = logConsoleProgress(`Querying databases`);
|
||||||
|
try {
|
||||||
|
if (window.authType === AuthType.AAD) {
|
||||||
|
databases = await readDatabasesWithARM();
|
||||||
|
} else {
|
||||||
|
const sdkResponse = await client()
|
||||||
|
.databases.readAll()
|
||||||
|
.fetchAll();
|
||||||
|
databases = sdkResponse.resources as DataModels.Database[];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleError(`Error while querying databases:\n ${JSON.stringify(error)}`);
|
||||||
|
logError(JSON.stringify(error), "ReadDatabases", error.code);
|
||||||
|
sendNotificationForError(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
clearMessage();
|
||||||
|
return databases;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readDatabasesWithARM(): Promise<DataModels.Database[]> {
|
||||||
|
let rpResponse;
|
||||||
|
const subscriptionId = userContext.subscriptionId;
|
||||||
|
const resourceGroup = userContext.resourceGroup;
|
||||||
|
const accountName = userContext.databaseAccount.name;
|
||||||
|
const defaultExperience = userContext.defaultExperience;
|
||||||
|
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case DefaultAccountExperienceType.DocumentDB:
|
||||||
|
rpResponse = await listSqlDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.MongoDB:
|
||||||
|
rpResponse = await listMongoDBDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Cassandra:
|
||||||
|
rpResponse = await listCassandraKeyspaces(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
case DefaultAccountExperienceType.Graph:
|
||||||
|
rpResponse = await listGremlinDatabases(subscriptionId, resourceGroup, accountName);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpResponse?.value?.map(database => database.properties?.resource as DataModels.Database);
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { updateOfferThroughputBeyondLimit } from "./updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
|
describe("updateOfferThroughputBeyondLimit", () => {
|
||||||
|
it("should call fetch", async () => {
|
||||||
|
window.fetch = jest.fn(() => {
|
||||||
|
return {
|
||||||
|
ok: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
window.dataExplorer = {
|
||||||
|
logConsoleData: jest.fn(),
|
||||||
|
deleteInProgressConsoleDataWithId: jest.fn(),
|
||||||
|
extensionEndpoint: jest.fn()
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
} as any;
|
||||||
|
await updateOfferThroughputBeyondLimit({
|
||||||
|
subscriptionId: "foo",
|
||||||
|
resourceGroup: "foo",
|
||||||
|
databaseAccountName: "foo",
|
||||||
|
databaseName: "foo",
|
||||||
|
throughput: 1000000000,
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
|
});
|
||||||
|
expect(window.fetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
52
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
52
src/Common/dataAccess/updateOfferThroughputBeyondLimit.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Platform, configContext } from "../../ConfigContext";
|
||||||
|
import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils";
|
||||||
|
import { AutoPilotOfferSettings } from "../../Contracts/DataModels";
|
||||||
|
import { logConsoleProgress, logConsoleInfo, logConsoleError } from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import { HttpHeaders } from "../Constants";
|
||||||
|
|
||||||
|
interface UpdateOfferThroughputRequest {
|
||||||
|
subscriptionId: string;
|
||||||
|
resourceGroup: string;
|
||||||
|
databaseAccountName: string;
|
||||||
|
databaseName: string;
|
||||||
|
collectionName?: string;
|
||||||
|
throughput: number;
|
||||||
|
offerIsRUPerMinuteThroughputEnabled: boolean;
|
||||||
|
offerAutopilotSettings?: AutoPilotOfferSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateOfferThroughputBeyondLimit(request: UpdateOfferThroughputRequest): Promise<void> {
|
||||||
|
if (configContext.platform !== Platform.Portal) {
|
||||||
|
throw new Error("Updating throughput beyond specified limit is not supported on this platform");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourceDescriptionInfo = request.collectionName
|
||||||
|
? `database ${request.databaseName} and container ${request.collectionName}`
|
||||||
|
: `database ${request.databaseName}`;
|
||||||
|
|
||||||
|
const clearMessage = logConsoleProgress(
|
||||||
|
`Requesting increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const explorer = window.dataExplorer;
|
||||||
|
const url = `${explorer.extensionEndpoint()}/api/offerthroughputrequest/updatebeyondspecifiedlimit`;
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
headers: { [authorizationHeader.header]: authorizationHeader.token, [HttpHeaders.contentType]: "application/json" }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
logConsoleInfo(
|
||||||
|
`Successfully requested an increase in throughput to ${request.throughput} for ${resourceDescriptionInfo}`
|
||||||
|
);
|
||||||
|
clearMessage();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const error = await response.json();
|
||||||
|
logConsoleError(`Failed to request an increase in throughput for ${request.throughput}: ${error.message}`);
|
||||||
|
clearMessage();
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
@ -312,17 +312,6 @@ export interface Query {
|
|||||||
query: string;
|
query: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateOfferThroughputRequest {
|
|
||||||
subscriptionId: string;
|
|
||||||
resourceGroup: string;
|
|
||||||
databaseAccountName: string;
|
|
||||||
databaseName: string;
|
|
||||||
collectionName: string;
|
|
||||||
throughput: number;
|
|
||||||
offerIsRUPerMinuteThroughputEnabled: boolean;
|
|
||||||
offerAutopilotSettings?: AutoPilotOfferSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AutoPilotOfferSettings {
|
export interface AutoPilotOfferSettings {
|
||||||
tier?: AutopilotTier;
|
tier?: AutopilotTier;
|
||||||
maximumTierThroughput?: number;
|
maximumTierThroughput?: number;
|
||||||
|
8
src/DefaultAccountExperienceType.ts
Normal file
8
src/DefaultAccountExperienceType.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export enum DefaultAccountExperienceType {
|
||||||
|
DocumentDB = "DocumentDB",
|
||||||
|
Graph = "Graph",
|
||||||
|
MongoDB = "MongoDB",
|
||||||
|
Table = "Table",
|
||||||
|
Cassandra = "Cassandra",
|
||||||
|
ApiForMongoDB = "Azure Cosmos DB for MongoDB API"
|
||||||
|
}
|
@ -49,6 +49,12 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
|||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||||
|
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
||||||
|
{
|
||||||
|
key: "feature.enableLinkInjection",
|
||||||
|
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||||
|
value: "true"
|
||||||
|
},
|
||||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||||
{
|
{
|
||||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||||
|
@ -163,8 +163,14 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
/>
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.canexceedmaximumvalue"
|
key="feature.enablecodeofconduct"
|
||||||
label="Can exceed max value"
|
label="Enable Code Of Conduct Acknowledgement"
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
checked={false}
|
||||||
|
key="feature.enableLinkInjection"
|
||||||
|
label="Enable Injecting Notebook Viewer Link into the first cell"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
@ -172,6 +178,12 @@ exports[`Feature panel renders all flags 1`] = `
|
|||||||
className="checkboxRow"
|
className="checkboxRow"
|
||||||
horizontalAlign="space-between"
|
horizontalAlign="space-between"
|
||||||
>
|
>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
checked={false}
|
||||||
|
key="feature.canexceedmaximumvalue"
|
||||||
|
label="Can exceed max value"
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enablefixedcollectionwithsharedthroughput"
|
key="feature.enablefixedcollectionwithsharedthroughput"
|
||||||
|
@ -17,7 +17,8 @@ describe("GalleryCardComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
showDownload: true,
|
showDownload: true,
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import React from "react";
|
||||||
|
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
|
||||||
|
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
describe("CodeOfConductComponent", () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let codeOfConductProps: CodeOfConductComponentProps;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
|
||||||
|
status: HttpStatusCodes.OK,
|
||||||
|
data: true
|
||||||
|
} as IJunoResponse<boolean>);
|
||||||
|
const junoClient = new JunoClient(undefined);
|
||||||
|
codeOfConductProps = {
|
||||||
|
junoClient: junoClient,
|
||||||
|
onAcceptCodeOfConduct: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onAcceptedCodeOfConductCalled", async () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
wrapper
|
||||||
|
.find(".genericPaneSubmitBtn")
|
||||||
|
.first()
|
||||||
|
.simulate("click");
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
112
src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx
Normal file
112
src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
||||||
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface CodeOfConductComponentProps {
|
||||||
|
junoClient: JunoClient;
|
||||||
|
onAcceptCodeOfConduct: (result: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeOfConductComponentState {
|
||||||
|
readCodeOfConduct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
||||||
|
private descriptionPara1: string;
|
||||||
|
private descriptionPara2: string;
|
||||||
|
private descriptionPara3: string;
|
||||||
|
private link1: { label: string; url: string };
|
||||||
|
private link2: { label: string; url: string };
|
||||||
|
|
||||||
|
constructor(props: CodeOfConductComponentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
readCodeOfConduct: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
||||||
|
this.descriptionPara2 =
|
||||||
|
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
|
||||||
|
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
|
||||||
|
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
|
||||||
|
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acceptCodeOfConduct(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const response = await this.props.junoClient.acceptCodeOfConduct();
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to accept code of conduct: ${error}`;
|
||||||
|
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
|
||||||
|
logConsoleError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChangeCheckbox = (): void => {
|
||||||
|
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 20 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Text style={{ fontWeight: 500, fontSize: "20px" }}>{this.descriptionPara1}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Text>{this.descriptionPara2}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Text>
|
||||||
|
{this.descriptionPara3}
|
||||||
|
<Link href={this.link1.url} target="_blank">
|
||||||
|
{this.link1.label}
|
||||||
|
</Link>
|
||||||
|
{" and "}
|
||||||
|
<Link href={this.link2.url} target="_blank">
|
||||||
|
{this.link2.label}
|
||||||
|
</Link>
|
||||||
|
</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<Checkbox
|
||||||
|
styles={{
|
||||||
|
label: {
|
||||||
|
margin: 0,
|
||||||
|
padding: "2 0 2 0"
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
|
onChange={this.onChangeCheckbox}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<PrimaryButton
|
||||||
|
ariaLabel="Continue"
|
||||||
|
title="Continue"
|
||||||
|
onClick={async () => await this.acceptCodeOfConduct()}
|
||||||
|
tabIndex={0}
|
||||||
|
className="genericPaneSubmitBtn"
|
||||||
|
text="Continue"
|
||||||
|
disabled={!this.state.readCodeOfConduct}
|
||||||
|
/>
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -15,7 +15,7 @@ import {
|
|||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } from "../../../Juno/JunoClient";
|
||||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
@ -24,6 +24,8 @@ import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/Gallery
|
|||||||
import "./GalleryViewerComponent.less";
|
import "./GalleryViewerComponent.less";
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||||
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
@ -60,6 +62,7 @@ interface GalleryViewerComponentState {
|
|||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
|
isCodeOfConductAccepted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryTabInfo {
|
interface GalleryTabInfo {
|
||||||
@ -86,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private publicNotebooks: IGalleryItem[];
|
private publicNotebooks: IGalleryItem[];
|
||||||
private favoriteNotebooks: IGalleryItem[];
|
private favoriteNotebooks: IGalleryItem[];
|
||||||
private publishedNotebooks: IGalleryItem[];
|
private publishedNotebooks: IGalleryItem[];
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
private columnCount: number;
|
private columnCount: number;
|
||||||
private rowCount: number;
|
private rowCount: number;
|
||||||
|
|
||||||
@ -100,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
selectedTab: props.selectedTab,
|
selectedTab: props.selectedTab,
|
||||||
sortBy: props.sortBy,
|
sortBy: props.sortBy,
|
||||||
searchText: props.searchText,
|
searchText: props.searchText,
|
||||||
dialogProps: undefined
|
dialogProps: undefined,
|
||||||
|
isCodeOfConductAccepted: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortingOptions = [
|
this.sortingOptions = [
|
||||||
@ -134,10 +139,21 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
tabs.push(
|
||||||
|
this.createPublicGalleryTab(
|
||||||
|
GalleryTab.PublicGallery,
|
||||||
|
this.state.publicNotebooks,
|
||||||
|
this.state.isCodeOfConductAccepted
|
||||||
|
)
|
||||||
|
);
|
||||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
|
|
||||||
|
// explicitly checking if isCodeOfConductAccepted is not false, as it is initially undefined.
|
||||||
|
// Displaying code of conduct component on gallery load should not be the default behavior.
|
||||||
|
if (this.state.isCodeOfConductAccepted !== false) {
|
||||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
onLinkClick: this.onPivotChange,
|
onLinkClick: this.onPivotChange,
|
||||||
@ -167,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTab(
|
||||||
|
tab: GalleryTab,
|
||||||
|
data: IGalleryItem[],
|
||||||
|
acceptedCodeOfConduct: boolean
|
||||||
|
): GalleryTabInfo {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
@ -174,6 +201,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||||
|
return acceptedCodeOfConduct === false ? (
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.props.junoClient}
|
||||||
|
onAcceptCodeOfConduct={(result: boolean) => {
|
||||||
|
this.setState({ isCodeOfConductAccepted: result });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
this.createTabContent(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
@ -187,8 +227,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
<Stack.Item styles={{ root: { minWidth: 200 } }}>
|
||||||
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
<Dropdown options={this.sortingOptions} selectedKey={this.state.sortBy} onChange={this.onDropdownChange} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
{this.props.container?.isGalleryPublishEnabled() && (
|
||||||
|
<Stack.Item>
|
||||||
|
<InfoComponent />
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{data && this.createCardsTabContent(data)}
|
{data && this.createCardsTabContent(data)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@ -254,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
const response = await this.props.junoClient.getPublicNotebooks();
|
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||||
|
if (this.props.container.isCodeOfConductEnabled()) {
|
||||||
|
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||||
|
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||||
|
this.publicNotebooks = response.data?.notebooksData;
|
||||||
|
} else {
|
||||||
|
response = await this.props.junoClient.getPublicNotebooks();
|
||||||
|
this.publicNotebooks = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publicNotebooks = response.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load public notebooks: ${error}`;
|
const message = `Failed to load public notebooks: ${error}`;
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||||
@ -268,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
|
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
|
||||||
|
isCodeOfConductAccepted: this.isCodeOfConductAccepted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
|||||||
|
|
||||||
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
||||||
const toSearch = searchText.trim().toUpperCase();
|
const toSearch = searchText.trim().toUpperCase();
|
||||||
const searchData: string[] = [
|
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
|
||||||
item.author.toUpperCase(),
|
|
||||||
item.description.toUpperCase(),
|
if (item.tags) {
|
||||||
item.name.toUpperCase(),
|
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
|
||||||
...item.tags?.map(tag => tag.toUpperCase())
|
}
|
||||||
];
|
|
||||||
|
|
||||||
for (const data of searchData) {
|
for (const data of searchData) {
|
||||||
if (data?.indexOf(toSearch) !== -1) {
|
if (data?.indexOf(toSearch) !== -1) {
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
@import "../../../../../less/Common/Constants.less";
|
||||||
|
.infoPanel, .infoPanelMain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoPanel {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel, .infoLabelMain {
|
||||||
|
padding-left: 5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-weight: 400
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain {
|
||||||
|
color: @AccentMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain:hover {
|
||||||
|
color: @BaseMedium
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { InfoComponent } from "./InfoComponent";
|
||||||
|
|
||||||
|
describe("InfoComponent", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<InfoComponent />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,42 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
|
||||||
|
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||||
|
import "./InfoComponent.less";
|
||||||
|
|
||||||
|
export class InfoComponent extends React.Component {
|
||||||
|
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Link href={url} target="_blank">
|
||||||
|
<div className="infoPanel">
|
||||||
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabel">{labelText}</Label>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHover = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
|
||||||
|
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||||
|
<div className="infoPanelMain">
|
||||||
|
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabelMain">Help</Label>
|
||||||
|
</div>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`InfoComponent renders 1`] = `
|
||||||
|
<StyledHoverCardBase
|
||||||
|
instantOpenOnClick={true}
|
||||||
|
plainCardProps={
|
||||||
|
Object {
|
||||||
|
"onRenderPlainCard": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="PlainCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="infoPanelMain"
|
||||||
|
>
|
||||||
|
<Memo(StyledIconBase)
|
||||||
|
className="infoIconMain"
|
||||||
|
iconName="Help"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledLabelBase
|
||||||
|
className="infoLabelMain"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</StyledLabelBase>
|
||||||
|
</div>
|
||||||
|
</StyledHoverCardBase>
|
||||||
|
`;
|
@ -0,0 +1,75 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CodeOfConductComponent renders 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "20px",
|
||||||
|
"fontWeight": 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/cosmos-code-of-conduct"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
code of conduct
|
||||||
|
</StyledLinkBase>
|
||||||
|
and
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/ms-privacy-policy"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
privacy statement
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"margin": 0,
|
||||||
|
"padding": "2 0 2 0",
|
||||||
|
},
|
||||||
|
"text": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
ariaLabel="Continue"
|
||||||
|
className="genericPaneSubmitBtn"
|
||||||
|
disabled={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
text="Continue"
|
||||||
|
title="Continue"
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
`;
|
@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
downloadButtonText: "Download",
|
downloadButtonText: "Download",
|
||||||
@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
},
|
},
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
downloadButtonText: "Download",
|
downloadButtonText: "Download",
|
||||||
|
@ -19,6 +19,7 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
|
|||||||
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
|
||||||
import "./NotebookViewerComponent.less";
|
import "./NotebookViewerComponent.less";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { NotebookV4 } from "@nteract/commutable/lib/v4";
|
||||||
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
|
||||||
|
|
||||||
export interface NotebookViewerComponentProps {
|
export interface NotebookViewerComponentProps {
|
||||||
@ -86,6 +87,7 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
const notebook: Notebook = await response.json();
|
const notebook: Notebook = await response.json();
|
||||||
|
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
|
||||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||||
this.setState({ content: notebook, showProgressBar: false });
|
this.setState({ content: notebook, showProgressBar: false });
|
||||||
|
|
||||||
@ -105,10 +107,21 @@ export class NotebookViewerComponent extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
|
||||||
|
if (!newCellId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const notebookV4 = notebook as NotebookV4;
|
||||||
|
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
|
||||||
|
delete notebookV4.cells[0];
|
||||||
|
notebook = notebookV4;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="notebookViewerContainer">
|
<div className="notebookViewerContainer">
|
||||||
{this.props.backNavigationText ? (
|
{this.props.backNavigationText !== undefined ? (
|
||||||
<Link onClick={this.props.onBackClick}>
|
<Link onClick={this.props.onBackClick}>
|
||||||
<Icon iconName="Back" /> {this.props.backNavigationText}
|
<Icon iconName="Back" /> {this.props.backNavigationText}
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -15,7 +15,9 @@ import CassandraAddCollectionPane from "./Panes/CassandraAddCollectionPane";
|
|||||||
import Database from "./Tree/Database";
|
import Database from "./Tree/Database";
|
||||||
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
import DeleteCollectionConfirmationPane from "./Panes/DeleteCollectionConfirmationPane";
|
||||||
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
import DeleteDatabaseConfirmationPane from "./Panes/DeleteDatabaseConfirmationPane";
|
||||||
import { readDatabases, readCollection, readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
import { readOffers, refreshCachedResources } from "../Common/DocumentClientUtilityBase";
|
||||||
|
import { readCollection } from "../Common/dataAccess/readCollection";
|
||||||
|
import { readDatabases } from "../Common/dataAccess/readDatabases";
|
||||||
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
import EditTableEntityPane from "./Panes/Tables/EditTableEntityPane";
|
||||||
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
import EnvironmentUtility from "../Common/EnvironmentUtility";
|
||||||
import GraphStylingPane from "./Panes/GraphStylingPane";
|
import GraphStylingPane from "./Panes/GraphStylingPane";
|
||||||
@ -203,11 +205,15 @@ export default class Explorer {
|
|||||||
public setupNotebooksPane: SetupNotebooksPane;
|
public setupNotebooksPane: SetupNotebooksPane;
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: ReactAdapter;
|
public publishNotebookPaneAdapter: ReactAdapter;
|
||||||
|
public copyNotebookPaneAdapter: ReactAdapter;
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
|
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||||
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
|
public isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
@ -408,8 +414,15 @@ export default class Explorer {
|
|||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||||
);
|
);
|
||||||
|
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
||||||
|
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
||||||
|
);
|
||||||
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
|
);
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
@ -471,7 +484,13 @@ export default class Explorer {
|
|||||||
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
this.notificationConsoleData = ko.observableArray<ConsoleData>([]);
|
||||||
this.defaultExperience = ko.observable<string>();
|
this.defaultExperience = ko.observable<string>();
|
||||||
this.databaseAccount.subscribe(databaseAccount => {
|
this.databaseAccount.subscribe(databaseAccount => {
|
||||||
this.defaultExperience(DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount));
|
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(
|
||||||
|
databaseAccount
|
||||||
|
);
|
||||||
|
this.defaultExperience(defaultExperience);
|
||||||
|
updateUserContext({
|
||||||
|
defaultExperience: DefaultExperienceUtility.mapDefaultExperienceStringToEnum(defaultExperience)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.isPreferredApiDocumentDB = ko.computed(() => {
|
this.isPreferredApiDocumentDB = ko.computed(() => {
|
||||||
@ -1403,7 +1422,7 @@ export default class Explorer {
|
|||||||
|
|
||||||
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
const refreshDatabases = (offers?: DataModels.Offer[]) => {
|
||||||
this._setLoadingStatusText("Fetching databases...");
|
this._setLoadingStatusText("Fetching databases...");
|
||||||
readDatabases(null /*options*/).then(
|
readDatabases().then(
|
||||||
(databases: DataModels.Database[]) => {
|
(databases: DataModels.Database[]) => {
|
||||||
this._setLoadingStatusText("Successfully fetched databases.");
|
this._setLoadingStatusText("Successfully fetched databases.");
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
@ -1719,6 +1738,7 @@ export default class Explorer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let clearMessage;
|
||||||
try {
|
try {
|
||||||
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
||||||
this.databaseAccount().id,
|
this.databaseAccount().id,
|
||||||
@ -1730,10 +1750,14 @@ export default class Explorer {
|
|||||||
notebookWorkspace.properties.status &&
|
notebookWorkspace.properties.status &&
|
||||||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||||
) {
|
) {
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||||
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
|
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(this.databaseAccount().id, "default");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
|
Logger.logError(error, "Explorer/ensureNotebookWorkspaceRunning");
|
||||||
|
NotificationConsoleUtils.logConsoleError(`Failed to initialize notebook workspace: ${JSON.stringify(error)}`);
|
||||||
|
} finally {
|
||||||
|
clearMessage && clearMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2292,7 +2316,7 @@ export default class Explorer {
|
|||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/uploadFile");
|
Logger.logError(error, "Explorer/uploadFile");
|
||||||
@ -2347,14 +2371,28 @@ export default class Explorer {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
|
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
await this.notebookManager.openPublishNotebookPane(
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
this.isCodeOfConductEnabled(),
|
||||||
|
this.isLinkInjectionEnabled()
|
||||||
|
);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
this.isPublishNotebookPaneEnabled(true);
|
this.isPublishNotebookPaneEnabled(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public copyNotebook(name: string, content: string): void {
|
||||||
|
if (this.notebookManager) {
|
||||||
|
this.notebookManager.openCopyNotebookPane(name, content);
|
||||||
|
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
|
||||||
|
this.isCopyNotebookPaneEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public showOkModalDialog(title: string, msg: string): void {
|
public showOkModalDialog(title: string, msg: string): void {
|
||||||
this._dialogProps({
|
this._dialogProps({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
@ -2711,6 +2749,7 @@ export default class Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
await this.resourceTree.initialize();
|
||||||
|
this.notebookManager?.refreshPinnedRepos();
|
||||||
if (this.notebookToImport) {
|
if (this.notebookToImport) {
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@ export class NotebookContentClient {
|
|||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = `${parent.path}/${name}`;
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
if (await this.checkIfFilepathExists(filepath)) {
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
throw new Error(`File already exists: ${filepath}`);
|
||||||
}
|
}
|
||||||
@ -116,12 +116,7 @@ export class NotebookContentClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
||||||
const basename = filepath.split("/").pop();
|
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
||||||
let parentDirPath = filepath
|
|
||||||
.split(basename)
|
|
||||||
.shift()
|
|
||||||
.replace(/\/$/, ""); // no trailling slash
|
|
||||||
|
|
||||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
const items = await this.fetchNotebookFiles(parentDirPath);
|
||||||
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import { getFullName } from "../../Utils/UserUtils";
|
|||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||||
|
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
|
||||||
|
|
||||||
export interface NotebookManagerOptions {
|
export interface NotebookManagerOptions {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
@ -49,6 +50,7 @@ export default class NotebookManager {
|
|||||||
|
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||||
|
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
|
||||||
|
|
||||||
public initialize(params: NotebookManagerOptions): void {
|
public initialize(params: NotebookManagerOptions): void {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
@ -90,6 +92,12 @@ export default class NotebookManager {
|
|||||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
|
||||||
|
this.params.container,
|
||||||
|
this.junoClient,
|
||||||
|
this.gitHubOAuthService
|
||||||
|
);
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
|
|
||||||
@ -108,12 +116,29 @@ export default class NotebookManager {
|
|||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPublishNotebookPane(
|
public refreshPinnedRepos(): void {
|
||||||
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openPublishNotebookPane(
|
||||||
name: string,
|
name: string,
|
||||||
content: string | ImmutableNotebook,
|
content: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement
|
parentDomElement: HTMLElement,
|
||||||
): void {
|
isCodeOfConductEnabled: boolean,
|
||||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
isLinkInjectionEnabled: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
await this.publishNotebookPaneAdapter.open(
|
||||||
|
name,
|
||||||
|
getFullName(),
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
isCodeOfConductEnabled,
|
||||||
|
isLinkInjectionEnabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
this.copyNotebookPaneAdapter.open(name, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
|
@ -13,8 +13,10 @@ import { List, Map } from "immutable";
|
|||||||
|
|
||||||
const fileName = "file";
|
const fileName = "file";
|
||||||
const notebookName = "file.ipynb";
|
const notebookName = "file.ipynb";
|
||||||
const filePath = `folder/${fileName}`;
|
const folderPath = "folder";
|
||||||
const notebookPath = `folder/${notebookName}`;
|
const filePath = `${folderPath}/${fileName}`;
|
||||||
|
const notebookPath = `${folderPath}/${notebookName}`;
|
||||||
|
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
||||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||||
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
||||||
const notebookRecord = makeNotebookRecord({
|
const notebookRecord = makeNotebookRecord({
|
||||||
@ -43,10 +45,8 @@ const notebookRecord = makeNotebookRecord({
|
|||||||
source: 'display(HTML("<h1>Sample html</h1>"))',
|
source: 'display(HTML("<h1>Sample html</h1>"))',
|
||||||
outputs: List.of({
|
outputs: List.of({
|
||||||
data: Object.freeze({
|
data: Object.freeze({
|
||||||
data: {
|
|
||||||
"text/html": "<h1>Sample output</h1>",
|
"text/html": "<h1>Sample output</h1>",
|
||||||
"text/plain": "<IPython.core.display.HTML object>"
|
"text/plain": "<IPython.core.display.HTML object>"
|
||||||
}
|
|
||||||
} as MediaBundle),
|
} as MediaBundle),
|
||||||
output_type: "display_data",
|
output_type: "display_data",
|
||||||
metadata: undefined
|
metadata: undefined
|
||||||
@ -82,6 +82,26 @@ describe("NotebookUtil", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFilePath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(gitHubFolderUri, fileName)).toEqual(gitHubFileUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getParentPath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(gitHubFileUri)).toEqual(gitHubFolderUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getName", () => {
|
describe("getName", () => {
|
||||||
it("works for jupyter file paths", () => {
|
it("works for jupyter file paths", () => {
|
||||||
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
|
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { StringUtils } from "../../Utils/StringUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
@ -70,6 +70,46 @@ export class NotebookUtil {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getFilePath(path: string, fileName: string): string {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
|
if (contentInfo) {
|
||||||
|
let path = fileName;
|
||||||
|
if (contentInfo.path) {
|
||||||
|
path = `${contentInfo.path}/${path}`;
|
||||||
|
}
|
||||||
|
return GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getParentPath(filepath: string): undefined | string {
|
||||||
|
const basename = NotebookUtil.getName(filepath);
|
||||||
|
if (basename) {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(filepath);
|
||||||
|
if (contentInfo) {
|
||||||
|
const parentPath = contentInfo.path.split(basename).shift();
|
||||||
|
if (parentPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitHubUtils.toContentUri(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
parentPath.replace(/\/$/, "") // no trailling slash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentPath = filepath.split(basename).shift();
|
||||||
|
if (parentPath) {
|
||||||
|
return parentPath.replace(/\/$/, ""); // no trailling slash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public static getName(path: string): undefined | string {
|
public static getName(path: string): undefined | string {
|
||||||
let relativePath: string = path;
|
let relativePath: string = path;
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
@ -102,25 +142,19 @@ export class NotebookUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
||||||
let codeCellCount = -1;
|
let codeCellIndex = 0;
|
||||||
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
||||||
const cellId = notebookObject.cellOrder.get(i);
|
const cellId = notebookObject.cellOrder.get(i);
|
||||||
if (cellId) {
|
if (cellId) {
|
||||||
const cell = notebookObject.cellMap.get(cellId);
|
const cell = notebookObject.cellMap.get(cellId);
|
||||||
if (cell && cell.cell_type === "code") {
|
if (cell?.cell_type === "code") {
|
||||||
codeCellCount++;
|
const displayOutput = (cell as ImmutableCodeCell)?.outputs?.find(
|
||||||
const codeCell = cell as ImmutableCodeCell;
|
output => output.output_type === "display_data" || output.output_type === "execute_result"
|
||||||
if (codeCell.outputs) {
|
);
|
||||||
const displayOutput = codeCell.outputs.find((output: ImmutableOutput) => {
|
|
||||||
if (output.output_type === "display_data" || output.output_type === "execute_result") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (displayOutput) {
|
if (displayOutput) {
|
||||||
return codeCellCount;
|
return codeCellIndex;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
codeCellIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
198
src/Explorer/Panes/CopyNotebookPane.tsx
Normal file
198
src/Explorer/Panes/CopyNotebookPane.tsx
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import ko from "knockout";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
|
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||||
|
import { IDropdownOption } from "office-ui-fabric-react";
|
||||||
|
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
type: "MyNotebooks" | "GitHub";
|
||||||
|
|
||||||
|
// GitHub
|
||||||
|
owner?: string;
|
||||||
|
repo?: string;
|
||||||
|
branch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyNotebookPaneAdapter implements ReactAdapter {
|
||||||
|
private static readonly BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
parameters: ko.Observable<number>;
|
||||||
|
private isOpened: boolean;
|
||||||
|
private isExecuting: boolean;
|
||||||
|
private formError: string;
|
||||||
|
private formErrorDetail: string;
|
||||||
|
private name: string;
|
||||||
|
private content: string;
|
||||||
|
private pinnedRepos: IPinnedRepo[];
|
||||||
|
private selectedLocation: Location;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private container: Explorer,
|
||||||
|
private junoClient: JunoClient,
|
||||||
|
private gitHubOAuthService: GitHubOAuthService
|
||||||
|
) {
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
if (!this.isOpened) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const genericPaneProps: GenericRightPaneProps = {
|
||||||
|
container: this.container,
|
||||||
|
formError: this.formError,
|
||||||
|
formErrorDetail: this.formErrorDetail,
|
||||||
|
id: "copynotebookpane",
|
||||||
|
isExecuting: this.isExecuting,
|
||||||
|
title: "Copy notebook",
|
||||||
|
submitButtonText: "OK",
|
||||||
|
onClose: () => this.close(),
|
||||||
|
onSubmit: () => this.submit()
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
||||||
|
name: this.name,
|
||||||
|
pinnedRepos: this.pinnedRepos,
|
||||||
|
onDropDownChange: this.onDropDownChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...genericPaneProps}>
|
||||||
|
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerRender(): void {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open(name: string, content: string): Promise<void> {
|
||||||
|
this.name = name;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
this.isOpened = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
if (this.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
const message = `Received HTTP ${response.status} when fetching pinned repos`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.length > 0) {
|
||||||
|
this.pinnedRepos = response.data;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async submit(): Promise<void> {
|
||||||
|
let destination: string = this.selectedLocation?.type;
|
||||||
|
let clearMessage: () => void;
|
||||||
|
this.isExecuting = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.selectedLocation) {
|
||||||
|
throw new Error(`No location selected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedLocation.type === "GitHub") {
|
||||||
|
destination = `${destination} - ${GitHubUtils.toRepoFullName(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo
|
||||||
|
)} - ${this.selectedLocation.branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
|
||||||
|
|
||||||
|
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
|
||||||
|
if (!notebookContentItem) {
|
||||||
|
throw new Error(`Failed to upload ${this.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.formError = `Failed to copy ${this.name} to ${destination}`;
|
||||||
|
this.formErrorDetail = `${error}`;
|
||||||
|
|
||||||
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
clearMessage && clearMessage();
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||||
|
let parent: NotebookContentItem;
|
||||||
|
switch (location.type) {
|
||||||
|
case "MyNotebooks":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
path: this.container.getNotebookBasePath(),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GitHub":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
path: GitHubUtils.toContentUri(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo,
|
||||||
|
this.selectedLocation.branch,
|
||||||
|
""
|
||||||
|
),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported location type ${location.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.container.uploadFile(this.name, this.content, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||||
|
this.selectedLocation = option?.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
private reset = (): void => {
|
||||||
|
this.isOpened = false;
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.formError = undefined;
|
||||||
|
this.formErrorDetail = undefined;
|
||||||
|
this.name = undefined;
|
||||||
|
this.content = undefined;
|
||||||
|
this.pinnedRepos = undefined;
|
||||||
|
this.selectedLocation = undefined;
|
||||||
|
};
|
||||||
|
}
|
119
src/Explorer/Panes/CopyNotebookPaneComponent.tsx
Normal file
119
src/Explorer/Panes/CopyNotebookPaneComponent.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import * as React from "react";
|
||||||
|
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Label,
|
||||||
|
Text,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownProps,
|
||||||
|
IDropdownOption,
|
||||||
|
SelectableOptionMenuItemType,
|
||||||
|
IRenderFunction,
|
||||||
|
ISelectableOption
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
type: "MyNotebooks" | "GitHub";
|
||||||
|
|
||||||
|
// GitHub
|
||||||
|
owner?: string;
|
||||||
|
repo?: string;
|
||||||
|
branch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CopyNotebookPaneProps {
|
||||||
|
name: string;
|
||||||
|
pinnedRepos: IPinnedRepo[];
|
||||||
|
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
|
||||||
|
private static readonly BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
const dropDownProps: IDropdownProps = {
|
||||||
|
label: "Location",
|
||||||
|
ariaLabel: "Location",
|
||||||
|
placeholder: "Select an option",
|
||||||
|
onRenderTitle: this.onRenderDropDownTitle,
|
||||||
|
onRenderOption: this.onRenderDropDownOption,
|
||||||
|
options: this.getDropDownOptions(),
|
||||||
|
onChange: this.props.onDropDownChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label htmlFor="notebookName">Name</Label>
|
||||||
|
<Text id="notebookName">{this.props.name}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Dropdown {...dropDownProps} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||||
|
return <span>{options.length && options[0].title}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||||
|
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getDropDownOptions = (): IDropdownOption[] => {
|
||||||
|
const options: IDropdownOption[] = [];
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "MyNotebooks-Item",
|
||||||
|
text: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
title: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
data: {
|
||||||
|
type: "MyNotebooks"
|
||||||
|
} as Location
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header-Divider",
|
||||||
|
text: undefined,
|
||||||
|
itemType: SelectableOptionMenuItemType.Divider
|
||||||
|
});
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header",
|
||||||
|
text: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
itemType: SelectableOptionMenuItemType.Header
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.pinnedRepos.forEach(pinnedRepo => {
|
||||||
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}`,
|
||||||
|
text: repoFullName,
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
pinnedRepo.branches.forEach(branch =>
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
||||||
|
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
|
||||||
|
title: `${repoFullName} - ${branch.name}`,
|
||||||
|
data: {
|
||||||
|
type: "GitHub",
|
||||||
|
owner: pinnedRepo.owner,
|
||||||
|
repo: pinnedRepo.name,
|
||||||
|
branch: branch.name
|
||||||
|
} as Location
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
}
|
@ -8,7 +8,6 @@ import Explorer from "../Explorer";
|
|||||||
|
|
||||||
export interface GenericRightPaneProps {
|
export interface GenericRightPaneProps {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
content: JSX.Element;
|
|
||||||
formError: string;
|
formError: string;
|
||||||
formErrorDetail: string;
|
formErrorDetail: string;
|
||||||
id: string;
|
id: string;
|
||||||
@ -17,6 +16,7 @@ export interface GenericRightPaneProps {
|
|||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
isSubmitButtonVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericRightPaneState {
|
export interface GenericRightPaneState {
|
||||||
@ -56,18 +56,18 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
>
|
>
|
||||||
<div className="panelContentWrapper">
|
<div className="panelContentWrapper">
|
||||||
{this.createPanelHeader()}
|
{this.renderPanelHeader()}
|
||||||
{this.createErrorSection()}
|
{this.renderErrorSection()}
|
||||||
{this.props.content}
|
{this.props.children}
|
||||||
{this.createPanelFooter()}
|
{this.renderPanelFooter()}
|
||||||
</div>
|
</div>
|
||||||
{this.createLoadingScreen()}
|
{this.renderLoadingScreen()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createPanelHeader = (): JSX.Element => {
|
private renderPanelHeader = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="firstdivbg headerline">
|
<div className="firstdivbg headerline">
|
||||||
<span id="databaseTitle">{this.props.title}</span>
|
<span id="databaseTitle">{this.props.title}</span>
|
||||||
@ -83,7 +83,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createErrorSection = (): JSX.Element => {
|
private renderErrorSection = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
|
||||||
<div className="warningErrorContent">
|
<div className="warningErrorContent">
|
||||||
@ -103,11 +103,12 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createPanelFooter = (): JSX.Element => {
|
private renderPanelFooter = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="paneFooter">
|
<div className="paneFooter">
|
||||||
<div className="leftpanel-okbut">
|
<div className="leftpanel-okbut">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
style={{ visibility: this.props.isSubmitButtonVisible ? "visible" : "hidden" }}
|
||||||
ariaLabel="Submit"
|
ariaLabel="Submit"
|
||||||
title="Submit"
|
title="Submit"
|
||||||
onClick={this.props.onSubmit}
|
onClick={this.props.onSubmit}
|
||||||
@ -120,7 +121,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private createLoadingScreen = (): JSX.Element => {
|
private renderLoadingScreen = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
|
||||||
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} />
|
||||||
|
@ -10,6 +10,8 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
|
|||||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||||
import { toJS } from "@nteract/commutable";
|
import { toJS } from "@nteract/commutable";
|
||||||
|
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
parameters: ko.Observable<number>;
|
parameters: ko.Observable<number>;
|
||||||
@ -26,6 +28,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
private imageSrc: string;
|
private imageSrc: string;
|
||||||
private notebookObject: ImmutableNotebook;
|
private notebookObject: ImmutableNotebook;
|
||||||
private parentDomElement: HTMLElement;
|
private parentDomElement: HTMLElement;
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
|
private isLinkInjectionEnabled: boolean;
|
||||||
|
|
||||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||||
this.parameters = ko.observable(Date.now());
|
this.parameters = ko.observable(Date.now());
|
||||||
@ -40,7 +44,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
const props: GenericRightPaneProps = {
|
const props: GenericRightPaneProps = {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
content: this.createContent(),
|
|
||||||
formError: this.formError,
|
formError: this.formError,
|
||||||
formErrorDetail: this.formErrorDetail,
|
formErrorDetail: this.formErrorDetail,
|
||||||
id: "publishnotebookpane",
|
id: "publishnotebookpane",
|
||||||
@ -48,33 +51,86 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
title: "Publish to gallery",
|
title: "Publish to gallery",
|
||||||
submitButtonText: "Publish",
|
submitButtonText: "Publish",
|
||||||
onClose: () => this.close(),
|
onClose: () => this.close(),
|
||||||
onSubmit: () => this.submit()
|
onSubmit: () => this.submit(),
|
||||||
|
isSubmitButtonVisible: this.isCodeOfConductAccepted
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GenericRightPaneComponent {...props} />;
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
|
notebookName: this.name,
|
||||||
|
notebookDescription: "",
|
||||||
|
notebookTags: "",
|
||||||
|
notebookAuthor: this.author,
|
||||||
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
|
notebookObject: this.notebookObject,
|
||||||
|
notebookParentDomElement: this.parentDomElement,
|
||||||
|
onChangeName: (newValue: string) => (this.name = newValue),
|
||||||
|
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||||
|
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||||
|
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||||
|
onError: this.createFormErrorForLargeImageSelection,
|
||||||
|
clearFormError: this.clearFormError
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...props}>
|
||||||
|
{!this.isCodeOfConductAccepted ? (
|
||||||
|
<div style={{ padding: "15px", marginTop: "10px" }}>
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.junoClient}
|
||||||
|
onAcceptCodeOfConduct={() => {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
this.triggerRender();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
||||||
|
)}
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(
|
public async open(
|
||||||
name: string,
|
name: string,
|
||||||
author: string,
|
author: string,
|
||||||
notebookContent: string | ImmutableNotebook,
|
notebookContent: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement
|
parentDomElement: HTMLElement,
|
||||||
): void {
|
isCodeOfConductEnabled: boolean,
|
||||||
|
isLinkInjectionEnabled: boolean
|
||||||
|
): Promise<void> {
|
||||||
|
if (isCodeOfConductEnabled) {
|
||||||
|
try {
|
||||||
|
const response = await this.junoClient.isCodeOfConductAccepted();
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isCodeOfConductAccepted = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to check if code of conduct was accepted: ${error}`;
|
||||||
|
Logger.logError(message, "PublishNotebookPaneAdapter/isCodeOfConductAccepted");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
if (typeof notebookContent === "string") {
|
if (typeof notebookContent === "string") {
|
||||||
this.content = notebookContent as string;
|
this.content = notebookContent as string;
|
||||||
} else {
|
} else {
|
||||||
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
|
this.content = JSON.stringify(toJS(notebookContent));
|
||||||
this.notebookObject = notebookContent;
|
this.notebookObject = notebookContent;
|
||||||
}
|
}
|
||||||
this.parentDomElement = parentDomElement;
|
this.parentDomElement = parentDomElement;
|
||||||
|
|
||||||
this.isOpened = true;
|
this.isOpened = true;
|
||||||
|
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,13 +158,12 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.tags?.split(","),
|
this.tags?.split(","),
|
||||||
this.author,
|
this.author,
|
||||||
this.imageSrc,
|
this.imageSrc,
|
||||||
this.content
|
this.content,
|
||||||
|
this.isLinkInjectionEnabled
|
||||||
);
|
);
|
||||||
if (!response.data) {
|
if (response.data) {
|
||||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
this.formErrorDetail = `${error}`;
|
this.formErrorDetail = `${error}`;
|
||||||
@ -142,25 +197,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
private createContent = (): JSX.Element => {
|
|
||||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
|
||||||
notebookName: this.name,
|
|
||||||
notebookDescription: "",
|
|
||||||
notebookTags: "",
|
|
||||||
notebookAuthor: this.author,
|
|
||||||
notebookCreatedDate: new Date().toISOString(),
|
|
||||||
notebookObject: this.notebookObject,
|
|
||||||
notebookParentDomElement: this.parentDomElement,
|
|
||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
|
||||||
onError: this.createFormErrorForLargeImageSelection,
|
|
||||||
clearFormError: this.clearFormError
|
|
||||||
};
|
|
||||||
|
|
||||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
private reset = (): void => {
|
private reset = (): void => {
|
||||||
this.isOpened = false;
|
this.isOpened = false;
|
||||||
this.isExecuting = false;
|
this.isExecuting = false;
|
||||||
@ -174,5 +210,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.imageSrc = undefined;
|
this.imageSrc = undefined;
|
||||||
this.notebookObject = undefined;
|
this.notebookObject = undefined;
|
||||||
this.parentDomElement = undefined;
|
this.parentDomElement = undefined;
|
||||||
|
this.isCodeOfConductAccepted = undefined;
|
||||||
|
this.isLinkInjectionEnabled = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ describe("PublishNotebookPaneComponent", () => {
|
|||||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||||
notebookObject: undefined,
|
notebookObject: undefined,
|
||||||
notebookParentDomElement: undefined,
|
notebookParentDomElement: undefined,
|
||||||
|
onChangeName: undefined,
|
||||||
onChangeDescription: undefined,
|
onChangeDescription: undefined,
|
||||||
onChangeTags: undefined,
|
onChangeTags: undefined,
|
||||||
onChangeImageSrc: undefined,
|
onChangeImageSrc: undefined,
|
||||||
|
@ -15,6 +15,7 @@ export interface PublishNotebookPaneProps {
|
|||||||
notebookCreatedDate: string;
|
notebookCreatedDate: string;
|
||||||
notebookObject: ImmutableNotebook;
|
notebookObject: ImmutableNotebook;
|
||||||
notebookParentDomElement: HTMLElement;
|
notebookParentDomElement: HTMLElement;
|
||||||
|
onChangeName: (newValue: string) => void;
|
||||||
onChangeDescription: (newValue: string) => void;
|
onChangeDescription: (newValue: string) => void;
|
||||||
onChangeTags: (newValue: string) => void;
|
onChangeTags: (newValue: string) => void;
|
||||||
onChangeImageSrc: (newValue: string) => void;
|
onChangeImageSrc: (newValue: string) => void;
|
||||||
@ -24,6 +25,7 @@ export interface PublishNotebookPaneProps {
|
|||||||
|
|
||||||
interface PublishNotebookPaneState {
|
interface PublishNotebookPaneState {
|
||||||
type: string;
|
type: string;
|
||||||
|
notebookName: string;
|
||||||
notebookDescription: string;
|
notebookDescription: string;
|
||||||
notebookTags: string;
|
notebookTags: string;
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
@ -40,6 +42,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
private static readonly maxImageSizeInMib = 1.5;
|
private static readonly maxImageSizeInMib = 1.5;
|
||||||
private descriptionPara1: string;
|
private descriptionPara1: string;
|
||||||
private descriptionPara2: string;
|
private descriptionPara2: string;
|
||||||
|
private nameProps: ITextFieldProps;
|
||||||
private descriptionProps: ITextFieldProps;
|
private descriptionProps: ITextFieldProps;
|
||||||
private tagsProps: ITextFieldProps;
|
private tagsProps: ITextFieldProps;
|
||||||
private thumbnailUrlProps: ITextFieldProps;
|
private thumbnailUrlProps: ITextFieldProps;
|
||||||
@ -52,6 +55,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: ImageTypes.Url,
|
type: ImageTypes.Url,
|
||||||
|
notebookName: props.notebookName,
|
||||||
notebookDescription: "",
|
notebookDescription: "",
|
||||||
notebookTags: "",
|
notebookTags: "",
|
||||||
imageSrc: undefined
|
imageSrc: undefined
|
||||||
@ -165,6 +169,17 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.nameProps = {
|
||||||
|
label: "Name",
|
||||||
|
ariaLabel: "Name",
|
||||||
|
defaultValue: this.props.notebookName,
|
||||||
|
required: true,
|
||||||
|
onChange: (event, newValue) => {
|
||||||
|
this.props.onChangeName(newValue);
|
||||||
|
this.setState({ notebookName: newValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.descriptionProps = {
|
this.descriptionProps = {
|
||||||
label: "Description",
|
label: "Description",
|
||||||
ariaLabel: "Description",
|
ariaLabel: "Description",
|
||||||
@ -245,6 +260,10 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
<Text>{this.descriptionPara2}</Text>
|
<Text>{this.descriptionPara2}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<TextField {...this.nameProps} />
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<TextField {...this.descriptionProps} />
|
<TextField {...this.descriptionProps} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
@ -276,7 +295,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
}}
|
}}
|
||||||
isFavorite={false}
|
isFavorite={false}
|
||||||
showDownload={true}
|
showDownload={true}
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
import * as Constants from "../../Common/Constants";
|
|
||||||
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
|
import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
|
||||||
import * as ko from "knockout";
|
import * as ko from "knockout";
|
||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as ViewModels from "../../Contracts/ViewModels";
|
import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
|
||||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
|
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
|
||||||
import InfoBubbleIcon from "../../../images/info-bubble.svg";
|
import { UploadItemsPaneComponent, UploadItemsPaneProps } from "./UploadItemsPaneComponent";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
|
const UPLOAD_FILE_SIZE_LIMIT = 2097152;
|
||||||
@ -35,19 +33,30 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props: GenericRightPaneProps = {
|
const genericPaneProps: GenericRightPaneProps = {
|
||||||
container: this.container,
|
container: this.container,
|
||||||
content: this.createContent(),
|
|
||||||
formError: this.formError,
|
formError: this.formError,
|
||||||
formErrorDetail: this.formErrorDetail,
|
formErrorDetail: this.formErrorDetail,
|
||||||
id: "uploaditemspane",
|
id: "uploaditemspane",
|
||||||
isExecuting: this.isExecuting,
|
isExecuting: this.isExecuting,
|
||||||
|
isSubmitButtonVisible: true,
|
||||||
title: "Upload Items",
|
title: "Upload Items",
|
||||||
submitButtonText: "Upload",
|
submitButtonText: "Upload",
|
||||||
onClose: () => this.close(),
|
onClose: () => this.close(),
|
||||||
onSubmit: () => this.submit()
|
onSubmit: () => this.submit()
|
||||||
};
|
};
|
||||||
return <GenericRightPaneComponent {...props} />;
|
|
||||||
|
const uploadItemsPaneProps: UploadItemsPaneProps = {
|
||||||
|
selectedFilesTitle: this.selectedFilesTitle,
|
||||||
|
updateSelectedFiles: this.updateSelectedFiles,
|
||||||
|
uploadFileData: this.uploadFileData
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GenericRightPaneComponent {...genericPaneProps}>
|
||||||
|
<UploadItemsPaneComponent {...uploadItemsPaneProps} />
|
||||||
|
</GenericRightPaneComponent>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public triggerRender(): void {
|
public triggerRender(): void {
|
||||||
@ -110,77 +119,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private createContent = (): JSX.Element => {
|
|
||||||
return <div className="panelContent">{this.createMainContentSection()}</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
private createMainContentSection = (): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<div className="paneMainContent">
|
|
||||||
<div className="renewUploadItemsHeader">
|
|
||||||
<span> Select JSON Files </span>
|
|
||||||
<span className="infoTooltip" role="tooltip" tabIndex={0}>
|
|
||||||
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
|
|
||||||
<span className="tooltiptext infoTooltipWidth">
|
|
||||||
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
|
|
||||||
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
|
|
||||||
can perform multiple upload operations for larger data sets.
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="importFilesTitle"
|
|
||||||
type="text"
|
|
||||||
disabled
|
|
||||||
value={this.selectedFilesTitle}
|
|
||||||
aria-label="Select JSON Files"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="importDocsInput"
|
|
||||||
title="Upload Icon"
|
|
||||||
multiple
|
|
||||||
accept="application/json"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={this.updateSelectedFiles}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
iconProps={{ iconName: "FolderHorizontal" }}
|
|
||||||
className="fileImportButton"
|
|
||||||
alt="Select JSON files to upload"
|
|
||||||
title="Select JSON files to upload"
|
|
||||||
onClick={this.onImportButtonClick}
|
|
||||||
onKeyPress={this.onImportButtonKeyPress}
|
|
||||||
/>
|
|
||||||
<div className="fileUploadSummaryContainer" hidden={this.uploadFileData.length === 0}>
|
|
||||||
<b>File upload status</b>
|
|
||||||
<table className="fileUploadSummary">
|
|
||||||
<thead>
|
|
||||||
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
|
|
||||||
<th>FILE NAME</th>
|
|
||||||
<th>STATUS</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{this.uploadFileData.map(
|
|
||||||
(data: UploadDetailsRecord): JSX.Element => {
|
|
||||||
return (
|
|
||||||
<tr className="fileUploadSummaryTuple" key={data.fileName}>
|
|
||||||
<td>{data.fileName}</td>
|
|
||||||
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.selectedFiles = event.target.files;
|
this.selectedFiles = event.target.files;
|
||||||
this._updateSelectedFilesTitle();
|
this._updateSelectedFilesTitle();
|
||||||
@ -212,21 +150,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
|
|||||||
return totalFileSize;
|
return totalFileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
|
|
||||||
return `${numSucceeded} items created, ${numFailed} errors`;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onImportButtonClick = (): void => {
|
|
||||||
document.getElementById("importDocsInput").click();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
|
||||||
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
|
|
||||||
this.onImportButtonClick();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private reset = (): void => {
|
private reset = (): void => {
|
||||||
this.isOpened = false;
|
this.isOpened = false;
|
||||||
this.isExecuting = false;
|
this.isExecuting = false;
|
||||||
|
97
src/Explorer/Panes/UploadItemsPaneComponent.tsx
Normal file
97
src/Explorer/Panes/UploadItemsPaneComponent.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import * as Constants from "../../Common/Constants";
|
||||||
|
import * as React from "react";
|
||||||
|
import { IconButton } from "office-ui-fabric-react/lib/Button";
|
||||||
|
import { UploadDetailsRecord } from "../../workers/upload/definitions";
|
||||||
|
import InfoBubbleIcon from "../../../images/info-bubble.svg";
|
||||||
|
|
||||||
|
export interface UploadItemsPaneProps {
|
||||||
|
selectedFilesTitle: string;
|
||||||
|
updateSelectedFiles: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
uploadFileData: UploadDetailsRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UploadItemsPaneComponent extends React.Component<UploadItemsPaneProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="panelContent">
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<div className="renewUploadItemsHeader">
|
||||||
|
<span> Select JSON Files </span>
|
||||||
|
<span className="infoTooltip" role="tooltip" tabIndex={0}>
|
||||||
|
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
|
||||||
|
<span className="tooltiptext infoTooltipWidth">
|
||||||
|
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of
|
||||||
|
JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB.
|
||||||
|
You can perform multiple upload operations for larger data sets.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="importFilesTitle"
|
||||||
|
type="text"
|
||||||
|
disabled
|
||||||
|
value={this.props.selectedFilesTitle}
|
||||||
|
aria-label="Select JSON Files"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="importDocsInput"
|
||||||
|
title="Upload Icon"
|
||||||
|
multiple
|
||||||
|
accept="application/json"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={this.props.updateSelectedFiles}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
iconProps={{ iconName: "FolderHorizontal" }}
|
||||||
|
className="fileImportButton"
|
||||||
|
alt="Select JSON files to upload"
|
||||||
|
title="Select JSON files to upload"
|
||||||
|
onClick={this.onImportButtonClick}
|
||||||
|
onKeyPress={this.onImportButtonKeyPress}
|
||||||
|
/>
|
||||||
|
<div className="fileUploadSummaryContainer" hidden={this.props.uploadFileData.length === 0}>
|
||||||
|
<b>File upload status</b>
|
||||||
|
<table className="fileUploadSummary">
|
||||||
|
<thead>
|
||||||
|
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
|
||||||
|
<th>FILE NAME</th>
|
||||||
|
<th>STATUS</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.uploadFileData.map(
|
||||||
|
(data: UploadDetailsRecord): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<tr className="fileUploadSummaryTuple" key={data.fileName}>
|
||||||
|
<td>{data.fileName}</td>
|
||||||
|
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
|
||||||
|
return `${numSucceeded} items created, ${numFailed} errors`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onImportButtonClick = (): void => {
|
||||||
|
document.getElementById("importDocsInput").click();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
|
||||||
|
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
|
||||||
|
this.onImportButtonClick();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
@ -22,6 +22,15 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
Would you like to publish and share "SampleNotebook" to the gallery?
|
Would you like to publish and share "SampleNotebook" to the gallery?
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Name"
|
||||||
|
defaultValue="SampleNotebook.ipynb"
|
||||||
|
label="Name"
|
||||||
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Description"
|
ariaLabel="Description"
|
||||||
@ -93,6 +102,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"id": undefined,
|
"id": undefined,
|
||||||
"isSample": false,
|
"isSample": false,
|
||||||
"name": "SampleNotebook.ipynb",
|
"name": "SampleNotebook.ipynb",
|
||||||
|
"newCellId": undefined,
|
||||||
"tags": Array [
|
"tags": Array [
|
||||||
"",
|
"",
|
||||||
],
|
],
|
||||||
|
@ -16,9 +16,10 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { updateOfferThroughputBeyondLimit, updateOffer } from "../../Common/DocumentClientUtilityBase";
|
import { updateOffer } from "../../Common/DocumentClientUtilityBase";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
const updateThroughputBeyondLimitWarningMessage: string = `
|
const updateThroughputBeyondLimitWarningMessage: string = `
|
||||||
You are about to request an increase in throughput beyond the pre-allocated capacity.
|
You are about to request an increase in throughput beyond the pre-allocated capacity.
|
||||||
@ -519,16 +520,15 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
this.maxRUs() <= SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
this.throughput() > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million
|
||||||
) {
|
) {
|
||||||
const requestPayload: DataModels.UpdateOfferThroughputRequest = {
|
const requestPayload = {
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
databaseName: this.database.id(),
|
databaseName: this.database.id(),
|
||||||
collectionName: undefined,
|
|
||||||
throughput: newThroughput,
|
throughput: newThroughput,
|
||||||
offerIsRUPerMinuteThroughputEnabled: false
|
offerIsRUPerMinuteThroughputEnabled: false
|
||||||
};
|
};
|
||||||
const updateOfferBeyondLimitPromise: Q.Promise<void> = updateOfferThroughputBeyondLimit(requestPayload).then(
|
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||||
() => {
|
() => {
|
||||||
this.database.offer().content.offerThroughput = originalThroughputValue;
|
this.database.offer().content.offerThroughput = originalThroughputValue;
|
||||||
this.throughput(originalThroughputValue);
|
this.throughput(originalThroughputValue);
|
||||||
@ -552,7 +552,7 @@ export default class DatabaseSettingsTab extends TabsBase implements ViewModels.
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
promises.push(updateOfferBeyondLimitPromise);
|
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||||
} else {
|
} else {
|
||||||
const newOffer: DataModels.Offer = {
|
const newOffer: DataModels.Offer = {
|
||||||
content: {
|
content: {
|
||||||
|
@ -30,6 +30,7 @@ import { configContext } from "../../ConfigContext";
|
|||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
|
|
||||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
account: DataModels.DatabaseAccount;
|
||||||
@ -122,6 +123,7 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
const saveLabel = "Save";
|
const saveLabel = "Save";
|
||||||
|
const copyToLabel = "Copy to ...";
|
||||||
const publishLabel = "Publish to gallery";
|
const publishLabel = "Publish to gallery";
|
||||||
const workspaceLabel = "No Workspace";
|
const workspaceLabel = "No Workspace";
|
||||||
const kernelLabel = "No Kernel";
|
const kernelLabel = "No Kernel";
|
||||||
@ -164,9 +166,17 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel
|
ariaLabel: saveLabel
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
iconName: "Copy",
|
||||||
|
onCommandClick: () => this.copyNotebook(),
|
||||||
|
commandButtonLabel: copyToLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: copyToLabel
|
||||||
|
},
|
||||||
{
|
{
|
||||||
iconName: "PublishContent",
|
iconName: "PublishContent",
|
||||||
onCommandClick: () => this.publishToGallery(),
|
onCommandClick: async () => await this.publishToGallery(),
|
||||||
commandButtonLabel: publishLabel,
|
commandButtonLabel: publishLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
@ -456,15 +466,27 @@ export default class NotebookTabV2 extends TabsBase {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private publishToGallery = () => {
|
private publishToGallery = async () => {
|
||||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
this.container.publishNotebook(
|
await this.container.publishNotebook(
|
||||||
notebookContent.name,
|
notebookContent.name,
|
||||||
notebookContent.content,
|
notebookContent.content,
|
||||||
this.notebookComponentAdapter.getNotebookParentElement()
|
this.notebookComponentAdapter.getNotebookParentElement()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private copyNotebook = () => {
|
||||||
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
|
let content: string;
|
||||||
|
if (typeof notebookContent.content === "string") {
|
||||||
|
content = notebookContent.content;
|
||||||
|
} else {
|
||||||
|
content = stringifyNotebook(toJS(notebookContent.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
private traceTelemetry(actionType: number) {
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
@ -17,13 +17,10 @@ import { Action } from "../../Shared/Telemetry/TelemetryConstants";
|
|||||||
import { PlatformType } from "../../PlatformType";
|
import { PlatformType } from "../../PlatformType";
|
||||||
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
import { RequestOptions } from "@azure/cosmos/dist-esm";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import {
|
import { updateOffer, updateCollection } from "../../Common/DocumentClientUtilityBase";
|
||||||
updateOfferThroughputBeyondLimit,
|
|
||||||
updateOffer,
|
|
||||||
updateCollection
|
|
||||||
} from "../../Common/DocumentClientUtilityBase";
|
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
import { updateOfferThroughputBeyondLimit } from "../../Common/dataAccess/updateOfferThroughputBeyondLimit";
|
||||||
|
|
||||||
const ttlWarning: string = `
|
const ttlWarning: string = `
|
||||||
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
|
The system will automatically delete items based on the TTL value (in seconds) you provide, without needing a delete operation explicitly issued by a client application.
|
||||||
@ -347,7 +344,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
if (!this.isAutoPilotSelected()) {
|
if (!this.isAutoPilotSelected()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const originalAutoPilotSettings = this.collection.offer().content.offerAutopilotSettings;
|
const originalAutoPilotSettings = this.collection?.offer()?.content?.offerAutopilotSettings;
|
||||||
if (!originalAutoPilotSettings) {
|
if (!originalAutoPilotSettings) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -1144,7 +1141,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
newThroughput > SharedConstants.CollectionCreation.DefaultCollectionRUs1Million &&
|
||||||
this.container != null
|
this.container != null
|
||||||
) {
|
) {
|
||||||
const requestPayload: DataModels.UpdateOfferThroughputRequest = {
|
const requestPayload = {
|
||||||
subscriptionId: userContext.subscriptionId,
|
subscriptionId: userContext.subscriptionId,
|
||||||
databaseAccountName: userContext.databaseAccount.name,
|
databaseAccountName: userContext.databaseAccount.name,
|
||||||
resourceGroup: userContext.resourceGroup,
|
resourceGroup: userContext.resourceGroup,
|
||||||
@ -1153,7 +1150,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
throughput: newThroughput,
|
throughput: newThroughput,
|
||||||
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
offerIsRUPerMinuteThroughputEnabled: isRUPerMinuteThroughputEnabled
|
||||||
};
|
};
|
||||||
const updateOfferBeyondLimitPromise: Q.Promise<void> = updateOfferThroughputBeyondLimit(requestPayload).then(
|
const updateOfferBeyondLimitPromise = updateOfferThroughputBeyondLimit(requestPayload).then(
|
||||||
() => {
|
() => {
|
||||||
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
this.collection.offer().content.offerThroughput = originalThroughputValue;
|
||||||
this.throughput(originalThroughputValue);
|
this.throughput(originalThroughputValue);
|
||||||
@ -1185,7 +1182,7 @@ export default class SettingsTab extends TabsBase implements ViewModels.WaitsFor
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
promises.push(updateOfferBeyondLimitPromise);
|
promises.push(Q(updateOfferBeyondLimitPromise));
|
||||||
} else {
|
} else {
|
||||||
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
const updateOfferPromise = updateOffer(this.collection.offer(), newOffer, headerOptions).then(
|
||||||
(updatedOffer: DataModels.Offer) => {
|
(updatedOffer: DataModels.Offer) => {
|
||||||
|
@ -12,7 +12,8 @@ import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"
|
|||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { readCollections, readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
import { readOffers, readOffer } from "../../Common/DocumentClientUtilityBase";
|
||||||
|
import { readCollections } from "../../Common/dataAccess/readCollections";
|
||||||
|
|
||||||
export default class Database implements ViewModels.Database {
|
export default class Database implements ViewModels.Database {
|
||||||
public nodeKind: string;
|
public nodeKind: string;
|
||||||
@ -259,7 +260,7 @@ export default class Database implements ViewModels.Database {
|
|||||||
let collectionVMs: Collection[] = [];
|
let collectionVMs: Collection[] = [];
|
||||||
let deferred: Q.Deferred<void> = Q.defer<void>();
|
let deferred: Q.Deferred<void> = Q.defer<void>();
|
||||||
|
|
||||||
readCollections(this).then(
|
readCollections(this.id()).then(
|
||||||
(collections: DataModels.Collection[]) => {
|
(collections: DataModels.Collection[]) => {
|
||||||
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
let collectionsToAddVMPromises: Q.Promise<any>[] = [];
|
||||||
let deltaCollections = this.getDeltaCollections(collections);
|
let deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
@ -7,6 +7,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
@ -33,6 +34,9 @@ import TabsBase from "../Tabs/TabsBase";
|
|||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
|
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||||
|
public static readonly GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
private static readonly DataTitle = "DATA";
|
private static readonly DataTitle = "DATA";
|
||||||
private static readonly NotebooksTitle = "NOTEBOOKS";
|
private static readonly NotebooksTitle = "NOTEBOOKS";
|
||||||
private static readonly PseudoDirPath = "PsuedoDir";
|
private static readonly PseudoDirPath = "PsuedoDir";
|
||||||
@ -104,7 +108,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.myNotebooksContentRoot = {
|
this.myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
path: this.container.getNotebookBasePath(),
|
path: this.container.getNotebookBasePath(),
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
@ -118,7 +122,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
|
|
||||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
this.gitHubNotebooksContentRoot = {
|
this.gitHubNotebooksContentRoot = {
|
||||||
name: "GitHub repos",
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
path: ResourceTreeAdapter.PseudoDirPath,
|
path: ResourceTreeAdapter.PseudoDirPath,
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
@ -563,6 +567,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Copy to ...",
|
||||||
|
iconSrc: CopyIcon,
|
||||||
|
onClick: () => this.copyNotebook(item)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Download",
|
label: "Download",
|
||||||
iconSrc: NotebookIcon,
|
iconSrc: NotebookIcon,
|
||||||
@ -574,6 +583,13 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (item: NotebookContentItem) => {
|
||||||
|
const content = await this.container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
this.container.copyNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
||||||
let items: TreeNodeMenuItem[] = [
|
let items: TreeNodeMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
@ -317,6 +317,13 @@ export class GitHubClient {
|
|||||||
objectExpression: `refs/heads/${branch}:${path || ""}`
|
objectExpression: `refs/heads/${branch}:${path || ""}`
|
||||||
} as ContentsQueryParams)) as ContentsQueryResponse;
|
} as ContentsQueryParams)) as ContentsQueryResponse;
|
||||||
|
|
||||||
|
if (!response.repository.object) {
|
||||||
|
return {
|
||||||
|
status: HttpStatusCodes.NotFound,
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let data: IGitHubFile | IGitHubFile[];
|
let data: IGitHubFile | IGitHubFile[];
|
||||||
const entries = response.repository.object.entries;
|
const entries = response.repository.object.entries;
|
||||||
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
||||||
|
@ -2,10 +2,11 @@ import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/
|
|||||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||||
import { from, Observable, of } from "rxjs";
|
import { from, Observable, of } from "rxjs";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
|
import * as Base64Utils from "../Utils/Base64Utils";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||||
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit, IGitHubBranch } from "./GitHubClient";
|
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||||
import UrlUtility from "../Common/UrlUtility";
|
import UrlUtility from "../Common/UrlUtility";
|
||||||
|
|
||||||
@ -131,7 +132,7 @@ export class GitHubContentProvider implements IContentProvider {
|
|||||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = btoa(stringifyNotebook(toJS(makeNotebookRecord())));
|
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
@ -195,34 +196,63 @@ export class GitHubContentProvider implements IContentProvider {
|
|||||||
return from(
|
return from(
|
||||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||||
try {
|
try {
|
||||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
let commitMsg: string;
|
||||||
|
if (content.status === HttpStatusCodes.NotFound) {
|
||||||
|
// We'll create a new file since it doesn't exist
|
||||||
|
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
||||||
|
if (!commitMsg) {
|
||||||
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||||
|
}
|
||||||
|
|
||||||
let updatedContent: string;
|
let updatedContent: string;
|
||||||
if (model.type === "notebook") {
|
if (model.type === "notebook") {
|
||||||
updatedContent = btoa(stringifyNotebook(model.content as Notebook));
|
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
||||||
} else if (model.type === "file") {
|
} else if (model.type === "file") {
|
||||||
updatedContent = model.content as string;
|
updatedContent = model.content as string;
|
||||||
if (model.format !== "base64") {
|
if (model.format !== "base64") {
|
||||||
updatedContent = btoa(updatedContent);
|
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GitHubContentProviderError("Unsupported content type");
|
throw new GitHubContentProviderError("Unsupported content type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitHubFile = content.data as IGitHubFile;
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
let gitHubFile: IGitHubFile;
|
||||||
gitHubFile.repo.owner,
|
if (content.data) {
|
||||||
gitHubFile.repo.name,
|
gitHubFile = content.data as IGitHubFile;
|
||||||
gitHubFile.branch.name,
|
|
||||||
gitHubFile.path,
|
|
||||||
commitMsg,
|
|
||||||
updatedContent,
|
|
||||||
gitHubFile.sha
|
|
||||||
);
|
|
||||||
if (response.status !== HttpStatusCodes.OK) {
|
|
||||||
throw new GitHubContentProviderError("Failed to update", response.status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path,
|
||||||
|
commitMsg,
|
||||||
|
updatedContent,
|
||||||
|
gitHubFile?.sha
|
||||||
|
);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
||||||
|
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gitHubFile) {
|
||||||
gitHubFile.commit = response.data;
|
gitHubFile.commit = response.data;
|
||||||
|
} else {
|
||||||
|
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path
|
||||||
|
);
|
||||||
|
if (contentResponse.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new GitHubContentProviderError("Failed to get content", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
gitHubFile = contentResponse.data as IGitHubFile;
|
||||||
|
}
|
||||||
|
|
||||||
return this.createSuccessAjaxResponse(
|
return this.createSuccessAjaxResponse(
|
||||||
HttpStatusCodes.OK,
|
HttpStatusCodes.OK,
|
||||||
|
@ -47,7 +47,8 @@ const sampleGalleryItems: IGalleryItem[] = [
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -185,7 +186,7 @@ describe("Gallery", () => {
|
|||||||
json: () => undefined as any
|
json: () => undefined as any
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await junoClient.getNotebook(id);
|
const response = await junoClient.getNotebookInfo(id);
|
||||||
|
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`);
|
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`);
|
||||||
@ -353,7 +354,7 @@ describe("Gallery", () => {
|
|||||||
json: () => undefined as any
|
json: () => undefined as any
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content);
|
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content, false);
|
||||||
|
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
expect(response.status).toBe(HttpStatusCodes.OK);
|
expect(response.status).toBe(HttpStatusCodes.OK);
|
||||||
|
@ -36,6 +36,16 @@ export interface IGalleryItem {
|
|||||||
downloads: number;
|
downloads: number;
|
||||||
favorites: number;
|
favorites: number;
|
||||||
views: number;
|
views: number;
|
||||||
|
newCellId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryData {
|
||||||
|
metadata: IPublicGalleryMetaData;
|
||||||
|
notebooksData: IGalleryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryMetaData {
|
||||||
|
acceptedCodeOfConduct: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserGallery {
|
export interface IUserGallery {
|
||||||
@ -162,7 +172,62 @@ export class JunoClient {
|
|||||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
// will be renamed once feature.enableCodeOfConduct flag is removed
|
||||||
|
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: IPublicGalleryData;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
||||||
|
|
||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
@ -292,12 +357,23 @@ export class JunoClient {
|
|||||||
tags: string[],
|
tags: string[],
|
||||||
author: string,
|
author: string,
|
||||||
thumbnailUrl: string,
|
thumbnailUrl: string,
|
||||||
content: string
|
content: string,
|
||||||
|
isLinkInjectionEnabled: boolean
|
||||||
): Promise<IJunoResponse<IGalleryItem>> {
|
): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
body: JSON.stringify({
|
body: isLinkInjectionEnabled
|
||||||
|
? JSON.stringify({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
tags,
|
||||||
|
author,
|
||||||
|
thumbnailUrl,
|
||||||
|
content: JSON.parse(content),
|
||||||
|
addLinkToNotebookViewer: isLinkInjectionEnabled
|
||||||
|
} as IPublishNotebookRequest)
|
||||||
|
: JSON.stringify({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
tags,
|
tags,
|
||||||
@ -310,6 +386,8 @@ export class JunoClient {
|
|||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -2,7 +2,7 @@ import "bootstrap/dist/css/bootstrap.css";
|
|||||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as ReactDOM from "react-dom";
|
import * as ReactDOM from "react-dom";
|
||||||
import { initializeConfiguration } from "../ConfigContext";
|
import { initializeConfiguration, configContext } from "../ConfigContext";
|
||||||
import {
|
import {
|
||||||
NotebookViewerComponent,
|
NotebookViewerComponent,
|
||||||
NotebookViewerComponentProps
|
NotebookViewerComponentProps
|
||||||
@ -17,28 +17,41 @@ const onInit = async () => {
|
|||||||
await initializeConfiguration();
|
await initializeConfiguration();
|
||||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||||
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
|
||||||
const backNavigationText = galleryViewerProps.selectedTab && GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
let backNavigationText: string;
|
||||||
|
let onBackClick: () => void;
|
||||||
|
if (galleryViewerProps.selectedTab !== undefined) {
|
||||||
|
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
|
||||||
|
onBackClick = () => (window.location.href = `${configContext.hostedExplorerURL}gallery.html`);
|
||||||
|
}
|
||||||
const hideInputs = notebookViewerProps.hideInputs;
|
const hideInputs = notebookViewerProps.hideInputs;
|
||||||
|
|
||||||
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
|
||||||
render(notebookUrl, backNavigationText, hideInputs);
|
|
||||||
|
|
||||||
const galleryItemId = notebookViewerProps.galleryItemId;
|
const galleryItemId = notebookViewerProps.galleryItemId;
|
||||||
|
let galleryItem: IGalleryItem;
|
||||||
|
|
||||||
if (galleryItemId) {
|
if (galleryItemId) {
|
||||||
const junoClient = new JunoClient();
|
const junoClient = new JunoClient();
|
||||||
const notebook = await junoClient.getNotebook(galleryItemId);
|
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
|
||||||
render(notebookUrl, backNavigationText, hideInputs, notebook.data);
|
galleryItem = galleryItemJunoResponse.data;
|
||||||
}
|
}
|
||||||
|
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = (notebookUrl: string, backNavigationText: string, hideInputs: boolean, galleryItem?: IGalleryItem) => {
|
const render = (
|
||||||
|
notebookUrl: string,
|
||||||
|
backNavigationText: string,
|
||||||
|
hideInputs: boolean,
|
||||||
|
galleryItem?: IGalleryItem,
|
||||||
|
onBackClick?: () => void
|
||||||
|
) => {
|
||||||
const props: NotebookViewerComponentProps = {
|
const props: NotebookViewerComponentProps = {
|
||||||
junoClient: galleryItem ? new JunoClient() : undefined,
|
junoClient: galleryItem ? new JunoClient() : undefined,
|
||||||
notebookUrl,
|
notebookUrl,
|
||||||
galleryItem,
|
galleryItem,
|
||||||
backNavigationText,
|
backNavigationText,
|
||||||
hideInputs,
|
hideInputs,
|
||||||
onBackClick: undefined,
|
onBackClick: onBackClick,
|
||||||
onTagClick: undefined
|
onTagClick: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,9 +20,13 @@ export class ConnectionStringParser {
|
|||||||
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
const matches: string[] = connectionStringPart.match(Constants.EndpointsRegex.mongoCompute);
|
||||||
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
accessInput.accountName = matches && matches.length > 1 && matches[2];
|
||||||
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
|
accessInput.apiKind = DataModels.ApiKind.MongoDBCompute;
|
||||||
} else if (RegExp(Constants.EndpointsRegex.cassandra).test(connectionStringPart)) {
|
} else if (Constants.EndpointsRegex.cassandra.some(regex => RegExp(regex).test(connectionStringPart))) {
|
||||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.cassandra)[1];
|
Constants.EndpointsRegex.cassandra.forEach(regex => {
|
||||||
|
if (RegExp(regex).test(connectionStringPart)) {
|
||||||
|
accessInput.accountName = connectionStringPart.match(regex)[1];
|
||||||
accessInput.apiKind = DataModels.ApiKind.Cassandra;
|
accessInput.apiKind = DataModels.ApiKind.Cassandra;
|
||||||
|
}
|
||||||
|
});
|
||||||
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
} else if (RegExp(Constants.EndpointsRegex.table).test(connectionStringPart)) {
|
||||||
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
accessInput.accountName = connectionStringPart.match(Constants.EndpointsRegex.table)[1];
|
||||||
accessInput.apiKind = DataModels.ApiKind.Table;
|
accessInput.apiKind = DataModels.ApiKind.Table;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as _ from "underscore";
|
import * as _ from "underscore";
|
||||||
import * as Constants from "../Common/Constants";
|
import * as Constants from "../Common/Constants";
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "../DefaultAccountExperienceType";
|
||||||
|
|
||||||
export class DefaultExperienceUtility {
|
export class DefaultExperienceUtility {
|
||||||
public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string {
|
public static getDefaultExperienceFromDatabaseAccount(databaseAccount: DataModels.DatabaseAccount): string {
|
||||||
@ -59,6 +60,25 @@ export class DefaultExperienceUtility {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static mapDefaultExperienceStringToEnum(defaultExperience: string): DefaultAccountExperienceType {
|
||||||
|
switch (defaultExperience) {
|
||||||
|
case Constants.DefaultAccountExperience.DocumentDB:
|
||||||
|
return DefaultAccountExperienceType.DocumentDB;
|
||||||
|
case Constants.DefaultAccountExperience.Graph:
|
||||||
|
return DefaultAccountExperienceType.Graph;
|
||||||
|
case Constants.DefaultAccountExperience.MongoDB:
|
||||||
|
return DefaultAccountExperienceType.MongoDB;
|
||||||
|
case Constants.DefaultAccountExperience.Table:
|
||||||
|
return DefaultAccountExperienceType.Table;
|
||||||
|
case Constants.DefaultAccountExperience.Cassandra:
|
||||||
|
return DefaultAccountExperienceType.Cassandra;
|
||||||
|
case Constants.DefaultAccountExperience.ApiForMongoDB:
|
||||||
|
return DefaultAccountExperienceType.ApiForMongoDB;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported default experience type: ${defaultExperience}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static _getDefaultExperience(kind: string, capabilities: DataModels.Capability[]): string {
|
private static _getDefaultExperience(kind: string, capabilities: DataModels.Capability[]): string {
|
||||||
const defaultDefaultExperience: string = Constants.DefaultAccountExperience.DocumentDB;
|
const defaultDefaultExperience: string = Constants.DefaultAccountExperience.DocumentDB;
|
||||||
const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind);
|
const defaultExperienceFromKind: string = DefaultExperienceUtility._getDefaultExperienceFromAccountKind(kind);
|
||||||
|
@ -3,6 +3,7 @@ import { sendMessage } from "../../Common/MessageHandler";
|
|||||||
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
import { MessageTypes } from "../../Contracts/ExplorerContracts";
|
||||||
import { appInsights } from "../appInsights";
|
import { appInsights } from "../appInsights";
|
||||||
import { configContext } from "../../ConfigContext";
|
import { configContext } from "../../ConfigContext";
|
||||||
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that persists telemetry data to the portal tables.
|
* Class that persists telemetry data to the portal tables.
|
||||||
@ -115,6 +116,9 @@ export default class TelemetryProcessor {
|
|||||||
|
|
||||||
private static getData(data?: any): any {
|
private static getData(data?: any): any {
|
||||||
return {
|
return {
|
||||||
|
// TODO: Need to `any` here since the window imports Explorer which can't be in strict mode yet
|
||||||
|
authType: (window as any).authType,
|
||||||
|
subscriptionId: userContext.subscriptionId,
|
||||||
platform: configContext.platform,
|
platform: configContext.platform,
|
||||||
...(data ? data : [])
|
...(data ? data : [])
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { DatabaseAccount } from "./Contracts/DataModels";
|
import { DatabaseAccount } from "./Contracts/DataModels";
|
||||||
|
import { DefaultAccountExperienceType } from "./DefaultAccountExperienceType";
|
||||||
|
|
||||||
interface UserContext {
|
interface UserContext {
|
||||||
masterKey?: string;
|
masterKey?: string;
|
||||||
@ -9,6 +10,7 @@ interface UserContext {
|
|||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
authorizationToken?: string;
|
authorizationToken?: string;
|
||||||
resourceToken?: string;
|
resourceToken?: string;
|
||||||
|
defaultExperience?: DefaultAccountExperienceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userContext: Readonly<UserContext> = {} as const;
|
const userContext: Readonly<UserContext> = {} as const;
|
||||||
|
11
src/Utils/Base64Utils.test.ts
Normal file
11
src/Utils/Base64Utils.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as Base64Utils from "./Base64Utils";
|
||||||
|
|
||||||
|
describe("Base64Utils", () => {
|
||||||
|
describe("utf8ToB64", () => {
|
||||||
|
it("should convert utf8 to base64", () => {
|
||||||
|
expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd"));
|
||||||
|
expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+");
|
||||||
|
expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k=");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
7
src/Utils/Base64Utils.ts
Normal file
7
src/Utils/Base64Utils.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const utf8ToB64 = (utf8Str: string): string => {
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => {
|
||||||
|
return String.fromCharCode(parseInt(args, 16));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
@ -16,7 +16,8 @@ const galleryItem: IGalleryItem = {
|
|||||||
isSample: false,
|
isSample: false,
|
||||||
downloads: 0,
|
downloads: 0,
|
||||||
favorites: 0,
|
favorites: 0,
|
||||||
views: 0
|
views: 0,
|
||||||
|
newCellId: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("GalleryUtils", () => {
|
describe("GalleryUtils", () => {
|
||||||
|
@ -162,6 +162,7 @@ export type DatabaseAccountGetResults = ARMResourceProperties & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* The system generated resource properties associated with SQL databases, SQL containers, Gremlin databases and Gremlin graphs. */
|
/* The system generated resource properties associated with SQL databases, SQL containers, Gremlin databases and Gremlin graphs. */
|
||||||
|
// TODO: ExtendedResourceProperties was missing some properties such as _self which was manually added. Need to fix this in the RP spec.
|
||||||
export interface ExtendedResourceProperties {
|
export interface ExtendedResourceProperties {
|
||||||
/* A system generated property. A unique identifier. */
|
/* A system generated property. A unique identifier. */
|
||||||
readonly _rid: string;
|
readonly _rid: string;
|
||||||
@ -169,6 +170,8 @@ export interface ExtendedResourceProperties {
|
|||||||
readonly _ts: unknown;
|
readonly _ts: unknown;
|
||||||
/* A system generated property representing the resource etag required for optimistic concurrency control. */
|
/* A system generated property representing the resource etag required for optimistic concurrency control. */
|
||||||
readonly _etag: string;
|
readonly _etag: string;
|
||||||
|
// TODO: This property was manually added. It should be auto-generated like the other properties.
|
||||||
|
readonly _self: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* An Azure Cosmos DB resource throughput. */
|
/* An Azure Cosmos DB resource throughput. */
|
||||||
|
@ -296,6 +296,10 @@
|
|||||||
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko if: isCopyNotebookPaneEnabled -->
|
||||||
|
<div data-bind="react: copyNotebookPaneAdapter"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
<!-- Global access token expiration dialog - Start -->
|
<!-- Global access token expiration dialog - Start -->
|
||||||
<div
|
<div
|
||||||
id="dataAccessTokenModal"
|
id="dataAccessTokenModal"
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
"./src/Contracts/Versions.ts",
|
"./src/Contracts/Versions.ts",
|
||||||
"./src/Controls/Heatmap/Heatmap.ts",
|
"./src/Controls/Heatmap/Heatmap.ts",
|
||||||
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
|
"./src/Controls/Heatmap/HeatmapDatatypes.ts",
|
||||||
|
"./src/DefaultAccountExperienceType.ts",
|
||||||
"./src/Definitions/globals.d.ts",
|
"./src/Definitions/globals.d.ts",
|
||||||
"./src/Definitions/html.d.ts",
|
"./src/Definitions/html.d.ts",
|
||||||
"./src/Definitions/jquery-ui.d.ts",
|
"./src/Definitions/jquery-ui.d.ts",
|
||||||
@ -66,6 +67,7 @@
|
|||||||
"./src/Shared/Telemetry/TelemetryProcessor.ts",
|
"./src/Shared/Telemetry/TelemetryProcessor.ts",
|
||||||
"./src/Shared/appInsights.ts",
|
"./src/Shared/appInsights.ts",
|
||||||
"./src/UserContext.ts",
|
"./src/UserContext.ts",
|
||||||
|
"./src/Utils/Base64Utils.ts",
|
||||||
"./src/Utils/GitHubUtils.ts",
|
"./src/Utils/GitHubUtils.ts",
|
||||||
"./src/Utils/MessageValidation.ts",
|
"./src/Utils/MessageValidation.ts",
|
||||||
"./src/Utils/OfferUtils.ts",
|
"./src/Utils/OfferUtils.ts",
|
||||||
|
@ -10,6 +10,7 @@ const CreateFileWebpack = require("create-file-webpack");
|
|||||||
const childProcess = require("child_process");
|
const childProcess = require("child_process");
|
||||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
|
const isCI = require("is-ci");
|
||||||
|
|
||||||
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
|
const gitSha = childProcess.execSync("git rev-parse HEAD").toString("utf8");
|
||||||
|
|
||||||
@ -214,8 +215,13 @@ module.exports = function(env = {}, argv = {}) {
|
|||||||
})
|
})
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
watch: isCI || mode === "production" ? false : true,
|
||||||
|
// Hack since it is hard to disable watch entirely with webpack dev server https://github.com/webpack/webpack-dev-server/issues/1251#issuecomment-654240734
|
||||||
|
watchOptions: isCI ? { poll: 24 * 60 * 60 * 1000 } : {},
|
||||||
devServer: {
|
devServer: {
|
||||||
hot: false,
|
hot: !isCI,
|
||||||
|
inline: !isCI,
|
||||||
|
liveReload: !isCI,
|
||||||
https: true,
|
https: true,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
port: envVars.PORT,
|
port: envVars.PORT,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user