Migrate notebooks workspaces to generated clients (#876)
This commit is contained in:
parent
8f6cac3d35
commit
fc9f4c5583
|
@ -5,7 +5,6 @@ import { Collection } from "../Contracts/ViewModels";
|
|||
import DocumentId from "../Explorer/Tree/DocumentId";
|
||||
import { updateUserContext } from "../UserContext";
|
||||
import { deleteDocument, getEndpoint, queryDocuments, readDocument, updateDocument } from "./MongoProxyClient";
|
||||
jest.mock("../ResourceProvider/ResourceProviderClient.ts");
|
||||
|
||||
const databaseId = "testDB";
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import * as ViewModels from "../Contracts/ViewModels";
|
|||
import { GitHubOAuthService } from "../GitHub/GitHubOAuthService";
|
||||
import { useSidePanel } from "../hooks/useSidePanel";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import { NotebookWorkspaceManager } from "../NotebookWorkspaceManager/NotebookWorkspaceManager";
|
||||
import { RouteHandler } from "../RouteHandlers/RouteHandler";
|
||||
import { ExplorerSettings } from "../Shared/ExplorerSettings";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
|
@ -26,6 +25,12 @@ import * as TelemetryProcessor from "../Shared/Telemetry/TelemetryProcessor";
|
|||
import { userContext } from "../UserContext";
|
||||
import { getCollectionName, getDatabaseName, getUploadName } from "../Utils/APITypeUtils";
|
||||
import { update } from "../Utils/arm/generatedClients/cosmos/databaseAccounts";
|
||||
import {
|
||||
get as getWorkspace,
|
||||
listByDatabaseAccount,
|
||||
listConnectionInfo,
|
||||
start,
|
||||
} from "../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
|
||||
import { stringToBlob } from "../Utils/BlobUtils";
|
||||
import { isCapabilityEnabled } from "../Utils/CapabilityUtils";
|
||||
|
@ -123,7 +128,6 @@ export default class Explorer {
|
|||
public isNotebookEnabled: ko.Observable<boolean>;
|
||||
public isNotebooksEnabledForAccount: ko.Observable<boolean>;
|
||||
public notebookServerInfo: ko.Observable<DataModels.NotebookWorkspaceConnectionInfo>;
|
||||
public notebookWorkspaceManager: NotebookWorkspaceManager;
|
||||
public sparkClusterConnectionInfo: ko.Observable<DataModels.SparkClusterConnectionInfo>;
|
||||
public isSynapseLinkUpdating: ko.Observable<boolean>;
|
||||
public memoryUsageInfo: ko.Observable<DataModels.MemoryUsageInfo>;
|
||||
|
@ -159,7 +163,6 @@ export default class Explorer {
|
|||
? this.refreshDatabaseForResourceToken()
|
||||
: this.refreshAllDatabases(true);
|
||||
RouteHandler.getInstance().initHandler();
|
||||
this.notebookWorkspaceManager = new NotebookWorkspaceManager();
|
||||
await this._refreshNotebooksEnabledStateForAccount();
|
||||
this.isNotebookEnabled(
|
||||
userContext.authType !== AuthType.ResourceToken &&
|
||||
|
@ -581,37 +584,19 @@ export default class Explorer {
|
|||
this._isInitializingNotebooks = true;
|
||||
|
||||
await this.ensureNotebookWorkspaceRunning();
|
||||
let connectionInfo: DataModels.NotebookWorkspaceConnectionInfo = {
|
||||
authToken: undefined,
|
||||
notebookServerEndpoint: undefined,
|
||||
};
|
||||
try {
|
||||
connectionInfo = await this.notebookWorkspaceManager.getNotebookConnectionInfoAsync(
|
||||
databaseAccount.id,
|
||||
const connectionInfo = await listConnectionInfo(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
} catch (error) {
|
||||
this._isInitializingNotebooks = false;
|
||||
handleError(
|
||||
error,
|
||||
"initNotebooks/getNotebookConnectionInfoAsync",
|
||||
`Failed to get notebook workspace connection info: ${getErrorMessage(error)}`
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
// Overwrite with feature flags
|
||||
if (userContext.features.notebookServerUrl) {
|
||||
connectionInfo.notebookServerEndpoint = userContext.features.notebookServerUrl;
|
||||
}
|
||||
|
||||
if (userContext.features.notebookServerToken) {
|
||||
connectionInfo.authToken = userContext.features.notebookServerToken;
|
||||
}
|
||||
|
||||
this.notebookServerInfo(connectionInfo);
|
||||
this.notebookServerInfo({
|
||||
notebookServerEndpoint: userContext.features.notebookServerUrl || connectionInfo.notebookServerEndpoint,
|
||||
authToken: userContext.features.notebookServerToken || connectionInfo.authToken,
|
||||
});
|
||||
this.notebookServerInfo.valueHasMutated();
|
||||
this.refreshNotebookList();
|
||||
}
|
||||
|
||||
this._isInitializingNotebooks = false;
|
||||
}
|
||||
|
@ -643,7 +628,11 @@ export default class Explorer {
|
|||
}
|
||||
|
||||
try {
|
||||
const workspaces = await this.notebookWorkspaceManager.getNotebookWorkspacesAsync(databaseAccount?.id);
|
||||
const { value: workspaces } = await listByDatabaseAccount(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name
|
||||
);
|
||||
return workspaces && workspaces.length > 0 && workspaces.some((workspace) => workspace.name === "default");
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "Explorer/_containsDefaultNotebookWorkspace");
|
||||
|
@ -658,8 +647,10 @@ export default class Explorer {
|
|||
|
||||
let clearMessage;
|
||||
try {
|
||||
const notebookWorkspace = await this.notebookWorkspaceManager.getNotebookWorkspaceAsync(
|
||||
userContext.databaseAccount.id,
|
||||
const notebookWorkspace = await getWorkspace(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
if (
|
||||
|
@ -669,7 +660,7 @@ export default class Explorer {
|
|||
notebookWorkspace.properties.status.toLowerCase() === "stopped"
|
||||
) {
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress("Initializing notebook workspace");
|
||||
await this.notebookWorkspaceManager.startNotebookWorkspaceAsync(userContext.databaseAccount.id, "default");
|
||||
await start(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Explorer/ensureNotebookWorkspaceRunning", "Failed to initialize notebook workspace");
|
||||
|
|
|
@ -6,6 +6,7 @@ import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
|||
import * as Logger from "../../Common/Logger";
|
||||
import * as DataModels from "../../Contracts/DataModels";
|
||||
import { userContext } from "../../UserContext";
|
||||
import { createOrUpdate, destroy } from "../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
|
||||
|
||||
export class NotebookContainerClient {
|
||||
|
@ -130,16 +131,18 @@ export class NotebookContainerClient {
|
|||
}
|
||||
|
||||
private async recreateNotebookWorkspaceAsync(): Promise<void> {
|
||||
const explorer = window.dataExplorer;
|
||||
const { databaseAccount } = userContext;
|
||||
if (!databaseAccount?.id) {
|
||||
throw new Error("DataExplorer not initialized");
|
||||
}
|
||||
|
||||
const notebookWorkspaceManager = explorer.notebookWorkspaceManager;
|
||||
try {
|
||||
await notebookWorkspaceManager.deleteNotebookWorkspaceAsync(databaseAccount?.id, "default");
|
||||
await notebookWorkspaceManager.createNotebookWorkspaceAsync(databaseAccount?.id, "default");
|
||||
await destroy(userContext.subscriptionId, userContext.resourceGroup, userContext.databaseAccount.name, "default");
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookContainerClient/recreateNotebookWorkspaceAsync");
|
||||
return Promise.reject(error);
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useSidePanel } from "../../../hooks/useSidePanel";
|
|||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import { createOrUpdate } from "../../../Utils/arm/generatedClients/cosmosNotebooks/notebookWorkspaces";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
|
||||
|
@ -56,8 +57,10 @@ export const SetupNoteBooksPanel: FunctionComponent<SetupNoteBooksPanelProps> =
|
|||
|
||||
try {
|
||||
setLoadingTrue();
|
||||
await explorer.notebookWorkspaceManager.createNotebookWorkspaceAsync(
|
||||
userContext.databaseAccount && userContext.databaseAccount.id,
|
||||
await createOrUpdate(
|
||||
userContext.subscriptionId,
|
||||
userContext.resourceGroup,
|
||||
userContext.databaseAccount.name,
|
||||
"default"
|
||||
);
|
||||
explorer.isAccountReady.valueHasMutated(); // re-trigger init notebooks
|
||||
|
|
|
@ -1,98 +0,0 @@
|
|||
import { ArmApiVersions } from "../Common/Constants";
|
||||
import { IResourceProviderClient, IResourceProviderClientFactory } from "../ResourceProvider/IResourceProviderClient";
|
||||
import * as Logger from "../Common/Logger";
|
||||
import {
|
||||
NotebookWorkspace,
|
||||
NotebookWorkspaceConnectionInfo,
|
||||
NotebookWorkspaceFeedResponse,
|
||||
} from "../Contracts/DataModels";
|
||||
import { ResourceProviderClientFactory } from "../ResourceProvider/ResourceProviderClientFactory";
|
||||
import { getErrorMessage } from "../Common/ErrorHandlingUtils";
|
||||
|
||||
export class NotebookWorkspaceManager {
|
||||
private resourceProviderClientFactory: IResourceProviderClientFactory<any>;
|
||||
|
||||
constructor() {
|
||||
this.resourceProviderClientFactory = new ResourceProviderClientFactory();
|
||||
}
|
||||
|
||||
public async getNotebookWorkspacesAsync(cosmosdbResourceId: string): Promise<NotebookWorkspace[]> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces`;
|
||||
try {
|
||||
const response = (await this.rpClient(uri).getAsync(
|
||||
uri,
|
||||
ArmApiVersions.documentDB
|
||||
)) as NotebookWorkspaceFeedResponse;
|
||||
return response && response.value;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspacesAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getNotebookWorkspaceAsync(
|
||||
cosmosdbResourceId: string,
|
||||
notebookWorkspaceId: string
|
||||
): Promise<NotebookWorkspace> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`;
|
||||
try {
|
||||
return (await this.rpClient(uri).getAsync(uri, ArmApiVersions.documentDB)) as NotebookWorkspace;
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookWorkspaceAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async createNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`;
|
||||
try {
|
||||
await this.rpClient(uri).putAsync(uri, ArmApiVersions.documentDB, { name: notebookWorkspaceId });
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/createNotebookWorkspaceAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}`;
|
||||
try {
|
||||
await this.rpClient(uri).deleteAsync(uri, ArmApiVersions.documentDB);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/deleteNotebookWorkspaceAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getNotebookConnectionInfoAsync(
|
||||
cosmosdbResourceId: string,
|
||||
notebookWorkspaceId: string
|
||||
): Promise<NotebookWorkspaceConnectionInfo> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/listConnectionInfo`;
|
||||
try {
|
||||
return await this.rpClient<NotebookWorkspaceConnectionInfo>(uri).postAsync(
|
||||
uri,
|
||||
ArmApiVersions.documentDB,
|
||||
undefined
|
||||
);
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/getNotebookConnectionInfoAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async startNotebookWorkspaceAsync(cosmosdbResourceId: string, notebookWorkspaceId: string): Promise<void> {
|
||||
const uri = `${cosmosdbResourceId}/notebookWorkspaces/${notebookWorkspaceId}/start`;
|
||||
try {
|
||||
return await this.rpClient(uri).postAsync(uri, ArmApiVersions.documentDB, undefined, {
|
||||
skipResourceValidation: true,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "NotebookWorkspaceManager/startNotebookWorkspaceAsync");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private rpClient<TResource>(uri: string): IResourceProviderClient<TResource> {
|
||||
return this.resourceProviderClientFactory.getOrCreate(uri);
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
import { IResourceProviderClient, IResourceProviderClientFactory } from "./IResourceProviderClient";
|
||||
|
||||
describe("IResourceProviderClient", () => {
|
||||
interface TestResource {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const expectedResult: TestResource = { id: "a" };
|
||||
const expectedReason: any = "error";
|
||||
|
||||
class SuccessClient implements IResourceProviderClient<TestResource> {
|
||||
public deleteAsync(url: string, apiVersion?: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
public getAsync(url: string, apiVersion?: string): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
resolve(expectedResult);
|
||||
});
|
||||
}
|
||||
|
||||
public postAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
resolve(expectedResult);
|
||||
});
|
||||
}
|
||||
|
||||
public putAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
resolve(expectedResult);
|
||||
});
|
||||
}
|
||||
|
||||
public patchAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
resolve(expectedResult);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ErrorClient implements IResourceProviderClient<TestResource> {
|
||||
public patchAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
reject(expectedReason);
|
||||
});
|
||||
}
|
||||
|
||||
public putAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
reject(expectedReason);
|
||||
});
|
||||
}
|
||||
|
||||
public postAsync(url: string, apiVersion: string, body: TestResource): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
reject(expectedReason);
|
||||
});
|
||||
}
|
||||
|
||||
public getAsync(url: string, apiVersion?: string): Promise<TestResource> {
|
||||
return new Promise<TestResource>((resolve, reject) => {
|
||||
reject(expectedReason);
|
||||
});
|
||||
}
|
||||
|
||||
public deleteAsync(url: string, apiVersion?: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
reject(expectedReason);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TestResourceProviderClientFactory implements IResourceProviderClientFactory<TestResource> {
|
||||
public getOrCreate(url: string): IResourceProviderClient<TestResource> {
|
||||
switch (url) {
|
||||
case "reject":
|
||||
return new ErrorClient();
|
||||
case "fulfill":
|
||||
default:
|
||||
return new SuccessClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const factory = new TestResourceProviderClientFactory();
|
||||
const fulfillClient = factory.getOrCreate("fulfill");
|
||||
const rejectClient = factory.getOrCreate("reject");
|
||||
const testApiVersion = "apiversion";
|
||||
|
||||
describe("deleteAsync", () => {
|
||||
it("returns a fulfilled promise on success", async () => {
|
||||
const result = await fulfillClient.deleteAsync("/foo", testApiVersion);
|
||||
expect(result).toEqual(undefined);
|
||||
});
|
||||
|
||||
it("returns a rejected promise with a reason on error", async () => {
|
||||
let result: any;
|
||||
try {
|
||||
result = await rejectClient.deleteAsync("/foo", testApiVersion);
|
||||
} catch (reason) {
|
||||
result = reason;
|
||||
}
|
||||
|
||||
expect(result).toEqual(expectedReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getAsync", () => {
|
||||
it("returns a fulfilled promise with a value on success", async () => {
|
||||
const result = await fulfillClient.getAsync("/foo", testApiVersion);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("returns a rejected promise with a reason on error", async () => {
|
||||
let result: any;
|
||||
try {
|
||||
result = await rejectClient.getAsync("/foo", testApiVersion);
|
||||
} catch (reason) {
|
||||
result = reason;
|
||||
}
|
||||
|
||||
expect(result).toEqual(expectedReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe("postAsync", () => {
|
||||
it("returns a fulfilled promise with a value on success", async () => {
|
||||
const result = await fulfillClient.postAsync("/foo", testApiVersion, {});
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("returns a rejected promise with a reason on error", async () => {
|
||||
let result: any;
|
||||
try {
|
||||
result = await rejectClient.postAsync("/foo", testApiVersion, {});
|
||||
} catch (reason) {
|
||||
result = reason;
|
||||
}
|
||||
|
||||
expect(result).toEqual(expectedReason);
|
||||
});
|
||||
});
|
||||
|
||||
describe("putAsync", () => {
|
||||
it("returns a fulfilled promise with a value on success", async () => {
|
||||
const result = await fulfillClient.putAsync("/foo", testApiVersion, {});
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it("returns a rejected promise with a reason on error", async () => {
|
||||
let result: any;
|
||||
try {
|
||||
result = await rejectClient.putAsync("/foo", testApiVersion, {});
|
||||
} catch (reason) {
|
||||
result = reason;
|
||||
}
|
||||
|
||||
expect(result).toEqual(expectedReason);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,30 +0,0 @@
|
|||
export interface IResourceProviderClient<TResource> {
|
||||
deleteAsync(url: string, apiVersion: string, requestOptions?: IResourceProviderRequestOptions): Promise<void>;
|
||||
getAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
queryString?: string,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<TResource | TResource[]>;
|
||||
postAsync(url: string, apiVersion: string, body: any, requestOptions?: IResourceProviderRequestOptions): Promise<any>;
|
||||
putAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
body: any,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<TResource>;
|
||||
patchAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
body: any,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<TResource>;
|
||||
}
|
||||
|
||||
export interface IResourceProviderRequestOptions {
|
||||
skipResourceValidation: boolean;
|
||||
}
|
||||
|
||||
export interface IResourceProviderClientFactory<TResult> {
|
||||
getOrCreate(url: string): IResourceProviderClient<TResult>;
|
||||
}
|
|
@ -1,217 +0,0 @@
|
|||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { HttpStatusCodes } from "../Common/Constants";
|
||||
import { IResourceProviderClient, IResourceProviderRequestOptions } from "./IResourceProviderClient";
|
||||
import { OperationStatus } from "../Contracts/DataModels";
|
||||
import { TokenProviderFactory } from "../TokenProviders/TokenProviderFactory";
|
||||
import * as UrlUtility from "../Common/UrlUtility";
|
||||
|
||||
export class ResourceProviderClient<T> implements IResourceProviderClient<T> {
|
||||
private httpClient: HttpClient;
|
||||
|
||||
constructor(private armEndpoint: string) {
|
||||
this.httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async getAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
queryString?: string,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<T | T[]> {
|
||||
let uri = `${this.armEndpoint}${url}?api-version=${apiVersion}`;
|
||||
if (queryString) {
|
||||
uri += `&${queryString}`;
|
||||
}
|
||||
return await this.httpClient.getAsync<T | T[]>(
|
||||
uri,
|
||||
Object.assign({}, { skipResourceValidation: false }, requestOptions)
|
||||
);
|
||||
}
|
||||
|
||||
public async postAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
body: any,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<any> {
|
||||
const fullUrl = UrlUtility.createUri(this.armEndpoint, url);
|
||||
return await this.httpClient.postAsync(
|
||||
`${fullUrl}?api-version=${apiVersion}`,
|
||||
body,
|
||||
Object.assign({}, { skipResourceValidation: false }, requestOptions)
|
||||
);
|
||||
}
|
||||
|
||||
public async putAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
body: any,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<T> {
|
||||
const fullUrl = UrlUtility.createUri(this.armEndpoint, url);
|
||||
return await this.httpClient.putAsync<T>(
|
||||
`${fullUrl}?api-version=${apiVersion}`,
|
||||
body,
|
||||
Object.assign({}, { skipResourceValidation: false }, requestOptions)
|
||||
);
|
||||
}
|
||||
|
||||
public async patchAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
body: any,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<T> {
|
||||
const fullUrl = UrlUtility.createUri(this.armEndpoint, url);
|
||||
return await this.httpClient.patchAsync<T>(
|
||||
`${fullUrl}?api-version=${apiVersion}`,
|
||||
body,
|
||||
Object.assign({}, { skipResourceValidation: false }, requestOptions)
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteAsync(
|
||||
url: string,
|
||||
apiVersion: string,
|
||||
requestOptions?: IResourceProviderRequestOptions
|
||||
): Promise<void> {
|
||||
const fullUrl = UrlUtility.createUri(this.armEndpoint, url);
|
||||
return await this.httpClient.deleteAsync(
|
||||
`${fullUrl}?api-version=${apiVersion}`,
|
||||
Object.assign({}, { skipResourceValidation: true }, requestOptions)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class HttpClient {
|
||||
private static readonly SUCCEEDED_STATUS = "Succeeded";
|
||||
private static readonly FAILED_STATUS = "Failed";
|
||||
private static readonly CANCELED_STATUS = "Canceled";
|
||||
private static readonly AZURE_ASYNC_OPERATION_HEADER = "azure-asyncoperation";
|
||||
private static readonly RETRY_AFTER_HEADER = "Retry-After";
|
||||
private static readonly DEFAULT_THROTTLE_WAIT_TIME_SECONDS = 5;
|
||||
|
||||
private tokenProvider: ViewModels.TokenProvider;
|
||||
|
||||
constructor() {
|
||||
this.tokenProvider = TokenProviderFactory.create();
|
||||
}
|
||||
|
||||
public async getAsync<T>(url: string, requestOptions: IResourceProviderRequestOptions): Promise<T> {
|
||||
const args: RequestInit = { method: "GET" };
|
||||
const response = await this.httpRequest(new Request(url, args), requestOptions);
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
public async postAsync(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise<any> {
|
||||
body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body;
|
||||
const args: RequestInit = { method: "POST", headers: { "Content-Type": "application/json" }, body };
|
||||
const response = await this.httpRequest(new Request(url, args), requestOptions);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
public async putAsync<T>(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise<T> {
|
||||
body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body;
|
||||
const args: RequestInit = { method: "PUT", headers: { "Content-Type": "application/json" }, body };
|
||||
const response = await this.httpRequest(new Request(url, args), requestOptions);
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
public async patchAsync<T>(url: string, body: any, requestOptions: IResourceProviderRequestOptions): Promise<T> {
|
||||
body = typeof body !== "string" && body !== undefined ? JSON.stringify(body) : body;
|
||||
const args: RequestInit = { method: "PATCH", headers: { "Content-Type": "application/json" }, body };
|
||||
const response = await this.httpRequest(new Request(url, args), requestOptions);
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
public async deleteAsync(url: string, requestOptions: IResourceProviderRequestOptions): Promise<void> {
|
||||
const args: RequestInit = { method: "DELETE" };
|
||||
await this.httpRequest(new Request(url, args), requestOptions);
|
||||
return null;
|
||||
}
|
||||
|
||||
public async httpRequest<T>(
|
||||
request: RequestInfo,
|
||||
requestOptions: IResourceProviderRequestOptions,
|
||||
numRetries: number = 12
|
||||
): Promise<Response> {
|
||||
const authHeader = await this.tokenProvider.getAuthHeader();
|
||||
authHeader &&
|
||||
authHeader.forEach((value: string, header: string) => {
|
||||
(request as Request).headers.append(header, value);
|
||||
});
|
||||
const response = await fetch(request);
|
||||
|
||||
if (response.status === HttpStatusCodes.Accepted) {
|
||||
const operationStatusUrl: string =
|
||||
response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER);
|
||||
const resource = await this.pollOperationAndGetResultAsync<T>(request, operationStatusUrl, requestOptions);
|
||||
return new Response(resource && JSON.stringify(resource));
|
||||
}
|
||||
|
||||
if (response.status === HttpStatusCodes.TooManyRequests && numRetries > 0) {
|
||||
// retry on throttles
|
||||
let waitTimeInSeconds = response.headers.has(HttpClient.RETRY_AFTER_HEADER)
|
||||
? parseInt(response.headers.get(HttpClient.RETRY_AFTER_HEADER))
|
||||
: HttpClient.DEFAULT_THROTTLE_WAIT_TIME_SECONDS;
|
||||
|
||||
return new Promise<Response>((resolve: (value: Response) => void, reject: (error: any) => void) => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const response = await this.httpRequest<T>(request, requestOptions, numRetries - 1);
|
||||
resolve(response);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
throw error;
|
||||
}
|
||||
}, waitTimeInSeconds * 1000);
|
||||
});
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
// RP sometimes returns HTTP 200 for async operations instead of HTTP 202 (e.g., on PATCH operations), so we need to check
|
||||
const operationStatusUrl: string =
|
||||
response.headers && response.headers.get(HttpClient.AZURE_ASYNC_OPERATION_HEADER);
|
||||
|
||||
if (operationStatusUrl) {
|
||||
const resource = await this.pollOperationAndGetResultAsync<T>(request, operationStatusUrl, requestOptions);
|
||||
return new Response(resource && JSON.stringify(resource));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return Promise.reject({ code: response.status, message: await response.text() });
|
||||
}
|
||||
|
||||
private async pollOperationAndGetResultAsync<T>(
|
||||
originalRequest: RequestInfo,
|
||||
operationStatusUrl: string,
|
||||
requestOptions: IResourceProviderRequestOptions
|
||||
): Promise<T> {
|
||||
const getOperationResult = async (resolve: (value: T) => void, reject: (error: any) => void) => {
|
||||
const operationStatus: OperationStatus = await this.getAsync<OperationStatus>(operationStatusUrl, requestOptions);
|
||||
if (!operationStatus) {
|
||||
return reject("Could not retrieve operation status");
|
||||
} else if (operationStatus.status === HttpClient.SUCCEEDED_STATUS) {
|
||||
let result;
|
||||
if (requestOptions?.skipResourceValidation === false) {
|
||||
result = await this.getAsync<T>((originalRequest as Request).url, requestOptions);
|
||||
}
|
||||
return resolve(result);
|
||||
} else if (
|
||||
operationStatus.status === HttpClient.CANCELED_STATUS ||
|
||||
operationStatus.status === HttpClient.FAILED_STATUS
|
||||
) {
|
||||
const errorMessage = operationStatus.error
|
||||
? JSON.stringify(operationStatus.error)
|
||||
: "Operation could not be completed";
|
||||
return reject(errorMessage);
|
||||
}
|
||||
// TODO: add exponential backup and timeout threshold
|
||||
setTimeout(getOperationResult, 1000, resolve, reject);
|
||||
};
|
||||
|
||||
return new Promise<T>(getOperationResult);
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { configContext } from "../ConfigContext";
|
||||
import { IResourceProviderClientFactory, IResourceProviderClient } from "./IResourceProviderClient";
|
||||
import { ResourceProviderClient } from "./ResourceProviderClient";
|
||||
|
||||
export class ResourceProviderClientFactory implements IResourceProviderClientFactory<any> {
|
||||
private armEndpoint: string;
|
||||
private cachedClients: { [url: string]: IResourceProviderClient<any> } = {};
|
||||
|
||||
constructor() {
|
||||
this.armEndpoint = configContext.ARM_ENDPOINT;
|
||||
}
|
||||
|
||||
public getOrCreate(url: string): IResourceProviderClient<any> {
|
||||
if (!url) {
|
||||
throw new Error("No resource provider client factory params specified");
|
||||
}
|
||||
if (!this.cachedClients[url]) {
|
||||
this.cachedClients[url] = new ResourceProviderClient(this.armEndpoint);
|
||||
}
|
||||
return this.cachedClients[url];
|
||||
}
|
||||
}
|
|
@ -37,11 +37,10 @@ export async function createOrUpdate(
|
|||
subscriptionId: string,
|
||||
resourceGroupName: string,
|
||||
accountName: string,
|
||||
notebookWorkspaceName: string,
|
||||
body: Types.NotebookWorkspaceCreateUpdateParameters
|
||||
notebookWorkspaceName: string
|
||||
): Promise<Types.NotebookWorkspace> {
|
||||
const path = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.DocumentDB/databaseAccounts/${accountName}/notebookWorkspaces/${notebookWorkspaceName}`;
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body });
|
||||
return armRequest({ host: configContext.ARM_ENDPOINT, path, method: "PUT", apiVersion, body: {} });
|
||||
}
|
||||
|
||||
/* Deletes the notebook workspace for a Cosmos DB account. */
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
Generated from: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/stable/2021-04-15/notebook.json
|
||||
*/
|
||||
|
||||
import { ARMResourceProperties } from "../cosmos/types";
|
||||
|
||||
/* Parameters to create a notebook workspace resource */
|
||||
export type NotebookWorkspaceCreateUpdateParameters = unknown;
|
||||
|
||||
|
@ -16,7 +18,7 @@ export interface NotebookWorkspaceListResult {
|
|||
}
|
||||
|
||||
/* A notebook workspace resource */
|
||||
export type NotebookWorkspace = unknown & {
|
||||
export type NotebookWorkspace = ARMResourceProperties & {
|
||||
/* Resource properties. */
|
||||
properties?: NotebookWorkspaceProperties;
|
||||
};
|
||||
|
|
|
@ -144,13 +144,13 @@ async function getOperationStatus(operationStatusUrl: string) {
|
|||
|
||||
const body = await response.json();
|
||||
const status = body.status;
|
||||
if (!status && response.status === 200) {
|
||||
return body;
|
||||
}
|
||||
if (status === "Canceled" || status === "Failed") {
|
||||
const errorMessage = body.error ? JSON.stringify(body.error) : "Operation could not be completed";
|
||||
const error = new Error(errorMessage);
|
||||
throw new AbortError(error);
|
||||
}
|
||||
if (response.status === 200) {
|
||||
return body;
|
||||
}
|
||||
throw new Error(`Operation Response: ${JSON.stringify(body)}. Retrying.`);
|
||||
}
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
"./src/Platform/Hosted/extractFeatures.test.ts",
|
||||
"./src/Platform/Hosted/extractFeatures.ts",
|
||||
"./src/ReactDevTools.ts",
|
||||
"./src/ResourceProvider/IResourceProviderClient.ts",
|
||||
"./src/SelfServe/Example/SelfServeExample.types.ts",
|
||||
"./src/SelfServe/SelfServeStyles.tsx",
|
||||
"./src/SelfServe/SqlX/SqlxTypes.ts",
|
||||
|
|
Loading…
Reference in New Issue