зеркало из https://github.com/Azure/BatchExplorer.git
StorageAccountService using authenticated requests
This commit is contained in:
Родитель
55e0746f8e
Коммит
66dd10d76e
|
@ -12,6 +12,7 @@ import { DependencyName } from "@batch/ui-common/lib/environment";
|
||||||
import { DefaultFormLayoutProvider, DefaultParameterTypeResolver } from "@batch/ui-react/lib/components/form";
|
import { DefaultFormLayoutProvider, DefaultParameterTypeResolver } from "@batch/ui-react/lib/components/form";
|
||||||
import { ConsoleLogger } from "@batch/ui-common/lib/logging";
|
import { ConsoleLogger } from "@batch/ui-common/lib/logging";
|
||||||
import { BrowserDependencyName } from "@batch/ui-react";
|
import { BrowserDependencyName } from "@batch/ui-react";
|
||||||
|
import { StorageAccountServiceImpl } from "@batch/ui-service";
|
||||||
import { registerIcons } from "app/config";
|
import { registerIcons } from "app/config";
|
||||||
import {
|
import {
|
||||||
AuthorizationHttpService,
|
AuthorizationHttpService,
|
||||||
|
@ -75,6 +76,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||||
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
||||||
[DependencyName.HttpClient]:
|
[DependencyName.HttpClient]:
|
||||||
() => new BatchExplorerHttpClient(authService),
|
() => new BatchExplorerHttpClient(authService),
|
||||||
|
[BrowserDependencyName.StorageAccountService]:
|
||||||
|
() => new StorageAccountServiceImpl(),
|
||||||
[BrowserDependencyName.ParameterTypeResolver]:
|
[BrowserDependencyName.ParameterTypeResolver]:
|
||||||
() => new DefaultParameterTypeResolver(),
|
() => new DefaultParameterTypeResolver(),
|
||||||
[BrowserDependencyName.FormLayoutProvider]:
|
[BrowserDependencyName.FormLayoutProvider]:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
export {
|
export {
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
initEnvironment,
|
initEnvironment,
|
||||||
initMockEnvironment,
|
|
||||||
destroyEnvironment,
|
destroyEnvironment,
|
||||||
} from "./environment/environment-util";
|
} from "./environment/environment-util";
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"jest": "^27.1.0",
|
"jest": "^27.1.0",
|
||||||
"jest-axe": "^5.0.1",
|
"jest-axe": "^5.0.1",
|
||||||
"jest-junit": "^12.2.0",
|
"jest-junit": "^12.2.0",
|
||||||
|
"jest-mock-extended": "^2.0.6",
|
||||||
"ts-jest": "^27.0.5"
|
"ts-jest": "^27.0.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
@ -3102,6 +3103,19 @@
|
||||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jest-mock-extended": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-KoDdjqwIp2phaOWB0hr4O+9HF7hIJx7O+Reefi3iGrNhUpzVkod9UozYTSanvbNvjFYIEH6noA2tIjc8IDpadw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ts-essentials": "^7.0.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0",
|
||||||
|
"typescript": "^3.0.0 || ^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-pnp-resolver": {
|
"node_modules/jest-pnp-resolver": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||||
|
@ -4454,6 +4468,15 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-essentials": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=3.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-jest": {
|
"node_modules/ts-jest": {
|
||||||
"version": "27.1.4",
|
"version": "27.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
|
||||||
|
@ -7222,6 +7245,15 @@
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"jest-mock-extended": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-KoDdjqwIp2phaOWB0hr4O+9HF7hIJx7O+Reefi3iGrNhUpzVkod9UozYTSanvbNvjFYIEH6noA2tIjc8IDpadw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ts-essentials": "^7.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest-pnp-resolver": {
|
"jest-pnp-resolver": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
|
||||||
|
@ -8257,6 +8289,12 @@
|
||||||
"punycode": "^2.1.1"
|
"punycode": "^2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ts-essentials": {
|
||||||
|
"version": "7.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz",
|
||||||
|
"integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"ts-jest": {
|
"ts-jest": {
|
||||||
"version": "27.1.4",
|
"version": "27.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.1.4.tgz",
|
||||||
|
|
|
@ -68,7 +68,6 @@
|
||||||
"react-dom": ">=16.13.1 <17.0.0",
|
"react-dom": ">=16.13.1 <17.0.0",
|
||||||
"tslib": "~2.3.1"
|
"tslib": "~2.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@batch/common-config": "^1.0.0",
|
"@batch/common-config": "^1.0.0",
|
||||||
"@batch/ui-common": "^1.0.0",
|
"@batch/ui-common": "^1.0.0",
|
||||||
|
@ -84,6 +83,7 @@
|
||||||
"jest": "^27.1.0",
|
"jest": "^27.1.0",
|
||||||
"jest-axe": "^5.0.1",
|
"jest-axe": "^5.0.1",
|
||||||
"jest-junit": "^12.2.0",
|
"jest-junit": "^12.2.0",
|
||||||
|
"jest-mock-extended": "^2.0.6",
|
||||||
"ts-jest": "^27.0.5"
|
"ts-jest": "^27.0.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { initMockBrowserEnvironment } from "../../../environment";
|
import { initMockBrowserEnvironment } from "../../../environment";
|
||||||
import { runAxe } from "../../../test-util/a11y";
|
import { runAxe } from "../../../test-util/a11y";
|
||||||
|
@ -23,6 +24,20 @@ describe("Dropdown form control", () => {
|
||||||
const ddEl = screen.getByRole("combobox");
|
const ddEl = screen.getByRole("combobox");
|
||||||
expect(ddEl).toBeDefined();
|
expect(ddEl).toBeDefined();
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
user.click(ddEl);
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(ddEl.getAttribute("aria-expanded")).toBe("true")
|
||||||
|
);
|
||||||
|
|
||||||
|
const options = screen.getAllByRole("option");
|
||||||
|
expect(options.length).toEqual(3);
|
||||||
|
expect(options.map((option) => option.textContent)).toEqual([
|
||||||
|
"ace",
|
||||||
|
"king",
|
||||||
|
"queen",
|
||||||
|
]);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await runAxe(container, {
|
await runAxe(container, {
|
||||||
rules: {
|
rules: {
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { Parameter } from "@batch/ui-common";
|
||||||
|
import { createForm, Form } from "@batch/ui-common/lib/form";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { UserEvent } from "@testing-library/user-event/dist/types/setup";
|
||||||
|
import * as React from "react";
|
||||||
|
import { initMockBrowserEnvironment } from "../../../environment";
|
||||||
|
import { runAxe } from "../../../test-util/a11y";
|
||||||
|
import {
|
||||||
|
StorageAccountDropdown,
|
||||||
|
} from "../parameter-type";
|
||||||
|
|
||||||
|
/* KLUDGE: the parameter has to be called "subscriptionId" until we can specify
|
||||||
|
* dependencies in parameter types
|
||||||
|
*/
|
||||||
|
type FakeFormValues = {
|
||||||
|
subscriptionId?: string;
|
||||||
|
storageAccountId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("Parameter type tests", () => {
|
||||||
|
let user: UserEvent;
|
||||||
|
let form: Form<FakeFormValues>;
|
||||||
|
let subParam: Parameter<FakeFormValues, "subscriptionId">;
|
||||||
|
beforeEach(() => {
|
||||||
|
initMockBrowserEnvironment();
|
||||||
|
user = userEvent.setup();
|
||||||
|
form = createForm<FakeFormValues>({ values: {} });
|
||||||
|
subParam = form.param("subscriptionId", "string");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("StorageAccountDropdown", () => {
|
||||||
|
let storageParam: Parameter<FakeFormValues, "storageAccountId">;
|
||||||
|
beforeEach(() => {
|
||||||
|
storageParam = form.param("storageAccountId", "string");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("simple dropdown", async () => {
|
||||||
|
render(<StorageAccountDropdown param={storageParam} />);
|
||||||
|
const element = screen.getByRole("combobox");
|
||||||
|
await user.click(element);
|
||||||
|
await waitFor(() => expectElementEnabled(element));
|
||||||
|
expect(screen.queryAllByRole("option")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dropdown with subscription", async () => {
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<SubscriptionDropdown param={subParam} />
|
||||||
|
<StorageAccountDropdown param={storageParam} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const subDropdown = screen.getByRole("combobox", {
|
||||||
|
name: /subscriptionId/,
|
||||||
|
});
|
||||||
|
const storageDropdown = screen.getByRole("combobox", {
|
||||||
|
name: /storageAccountId/,
|
||||||
|
});
|
||||||
|
await user.click(subDropdown);
|
||||||
|
await waitFor(() => {
|
||||||
|
expectElementEnabled(subDropdown);
|
||||||
|
expectElementEnabled(storageDropdown);
|
||||||
|
});
|
||||||
|
selectOption(0);
|
||||||
|
await user.click(storageDropdown);
|
||||||
|
|
||||||
|
let storageAccounts = await screen.findAllByRole("option");
|
||||||
|
|
||||||
|
// Data served by FakeStorageAccountService
|
||||||
|
expect(storageAccounts.length).toEqual(3);
|
||||||
|
expect(storageAccounts[0].textContent).toEqual("Storage A");
|
||||||
|
|
||||||
|
await user.click(subDropdown); // Reopen sub dropdown
|
||||||
|
selectOption(2);
|
||||||
|
await user.click(storageDropdown);
|
||||||
|
|
||||||
|
storageAccounts = await screen.findAllByRole("option");
|
||||||
|
|
||||||
|
expect(storageAccounts.length).toEqual(4);
|
||||||
|
expect(storageAccounts[0].textContent).toEqual("Storage F");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bad subscription shows error", async () => {
|
||||||
|
render(
|
||||||
|
<>
|
||||||
|
<SubscriptionDropdown param={subParam} />
|
||||||
|
<StorageAccountDropdown param={storageParam} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
const subDropdown = screen.getByRole("combobox", {
|
||||||
|
name: /subscriptionId/,
|
||||||
|
});
|
||||||
|
const storageDropdown = screen.getByRole("combobox", {
|
||||||
|
name: /storageAccountId/,
|
||||||
|
});
|
||||||
|
await user.click(subDropdown);
|
||||||
|
await waitFor(() => {
|
||||||
|
expectElementEnabled(subDropdown);
|
||||||
|
expectElementEnabled(storageDropdown);
|
||||||
|
});
|
||||||
|
selectOption(4); // Bad subscription
|
||||||
|
expect(screen.getByText("Bad Subscription")).toBeDefined();
|
||||||
|
|
||||||
|
await user.click(storageDropdown);
|
||||||
|
|
||||||
|
expect(await screen.queryAllByRole("option")).toEqual([]);
|
||||||
|
expect(
|
||||||
|
screen.getByText("Error: No storage accounts in subscription.")
|
||||||
|
).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectElementEnabled = (element: HTMLElement) =>
|
||||||
|
expect(element.className).not.toContain("is-disabled");
|
||||||
|
|
||||||
|
const selectOption = (index: number) =>
|
||||||
|
fireEvent.click(screen.getAllByRole("option")[index]);
|
|
@ -1,10 +1,16 @@
|
||||||
import { ParameterType as CommonParameterType } from "@batch/ui-common";
|
import {
|
||||||
|
Parameter,
|
||||||
|
ParameterType as CommonParameterType,
|
||||||
|
} from "@batch/ui-common";
|
||||||
|
import { inject } from "@batch/ui-common/lib/environment";
|
||||||
|
import { FormValues, ValidationStatus } from "@batch/ui-common/lib/form";
|
||||||
|
import { StorageAccount, StorageAccountService } from "@batch/ui-service";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Parameter } from "@batch/ui-common";
|
import { useEffect, useState } from "react";
|
||||||
import { TextField } from "./text-field";
|
import { BrowserDependencyName } from "../..";
|
||||||
import { Dropdown } from "./dropdown";
|
|
||||||
import { useAsyncEffect, useUniqueId } from "../../hooks";
|
import { useAsyncEffect, useUniqueId } from "../../hooks";
|
||||||
import { FormValues } from "@batch/ui-common/lib/form";
|
import { Dropdown } from "./dropdown";
|
||||||
|
import { TextField } from "./text-field";
|
||||||
|
|
||||||
enum ExtendedParameterType {
|
enum ExtendedParameterType {
|
||||||
BatchAccountName = "BatchAccountName",
|
BatchAccountName = "BatchAccountName",
|
||||||
|
@ -99,7 +105,7 @@ export class DefaultParameterTypeResolver implements ParameterTypeResolver {
|
||||||
);
|
);
|
||||||
case ParameterType.StorageAccountId:
|
case ParameterType.StorageAccountId:
|
||||||
return (
|
return (
|
||||||
<StringParamTextField
|
<StorageAccountDropdown
|
||||||
id={id}
|
id={id}
|
||||||
key={param.name}
|
key={param.name}
|
||||||
param={param}
|
param={param}
|
||||||
|
@ -107,7 +113,7 @@ export class DefaultParameterTypeResolver implements ParameterTypeResolver {
|
||||||
);
|
);
|
||||||
case ParameterType.SubscriptionId:
|
case ParameterType.SubscriptionId:
|
||||||
return (
|
return (
|
||||||
<SubscriptionIdParamDropdown
|
<SubscriptionDropdown
|
||||||
id={id}
|
id={id}
|
||||||
key={param.name}
|
key={param.name}
|
||||||
param={param}
|
param={param}
|
||||||
|
@ -157,6 +163,68 @@ export function StringParamTextField<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StorageAccountDropdown<
|
||||||
|
V extends FormValues,
|
||||||
|
K extends Extract<keyof V, string>
|
||||||
|
>(props: ParamControlProps<V, K>): JSX.Element {
|
||||||
|
const { param } = props;
|
||||||
|
const value = param.value == null ? undefined : String(param.value);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [storageAccounts, setStorageAccounts] = useState<StorageAccount[]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const id = useUniqueId("form-control", props.id);
|
||||||
|
const service: StorageAccountService = inject(
|
||||||
|
BrowserDependencyName.StorageAccountService
|
||||||
|
);
|
||||||
|
const form = param.parentForm;
|
||||||
|
const [subscriptionId, setSubscriptionId] = useState<string>(
|
||||||
|
form.values.subscriptionId as string
|
||||||
|
);
|
||||||
|
const [validationStatus, setValidationStatus] =
|
||||||
|
useState<ValidationStatus | null>();
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
let accounts: StorageAccount[] = [];
|
||||||
|
try {
|
||||||
|
if (subscriptionId) {
|
||||||
|
accounts = await service.list(subscriptionId);
|
||||||
|
}
|
||||||
|
setValidationStatus(null);
|
||||||
|
} catch (error) {
|
||||||
|
setValidationStatus(new ValidationStatus("error", error + ""));
|
||||||
|
} finally {
|
||||||
|
setStorageAccounts(accounts);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [subscriptionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = form.onChange((values: FormValues) =>
|
||||||
|
setSubscriptionId(values.subscriptionId as string)
|
||||||
|
);
|
||||||
|
return () => form.removeOnChange(handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
const options = storageAccounts.map((sub) => {
|
||||||
|
return { value: sub.id, label: sub.name };
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
id={id}
|
||||||
|
label={param.label}
|
||||||
|
disabled={loading || param.disabled}
|
||||||
|
options={options}
|
||||||
|
placeholder={param.placeholder}
|
||||||
|
value={value}
|
||||||
|
validationStatus={validationStatus ?? param.validationStatus}
|
||||||
|
onChange={(value: string) => (param.value = value as V[K])}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SubscriptionIdParamDropdown<
|
export function SubscriptionIdParamDropdown<
|
||||||
V extends FormValues,
|
V extends FormValues,
|
||||||
K extends Extract<keyof V, string>
|
K extends Extract<keyof V, string>
|
||||||
|
@ -183,7 +251,7 @@ export function SubscriptionIdParamDropdown<
|
||||||
resolve();
|
resolve();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
});
|
});
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
const options = subscriptions.map((sub) => {
|
const options = subscriptions.map((sub) => {
|
||||||
return { value: sub.id, label: sub.displayName };
|
return { value: sub.id, label: sub.displayName };
|
||||||
|
@ -197,9 +265,7 @@ export function SubscriptionIdParamDropdown<
|
||||||
options={options}
|
options={options}
|
||||||
placeholder={param.placeholder}
|
placeholder={param.placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(newValue: string) => {
|
onChange={(newValue: string) => (param.value = newValue as V[K])}
|
||||||
param.value = newValue as V[K];
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
} from "@batch/ui-common/lib/environment";
|
} from "@batch/ui-common/lib/environment";
|
||||||
import { FormValues } from "@batch/ui-common/lib/form";
|
import { FormValues } from "@batch/ui-common/lib/form";
|
||||||
|
import { StorageAccountService } from "@batch/ui-service";
|
||||||
import {
|
import {
|
||||||
FormLayout,
|
FormLayout,
|
||||||
FormLayoutProvider,
|
FormLayoutProvider,
|
||||||
|
@ -23,6 +24,7 @@ import { MockBrowserEnvironment } from "./mock-browser-environment";
|
||||||
export enum BrowserDependencyName {
|
export enum BrowserDependencyName {
|
||||||
ParameterTypeResolver = "parameterTypeResolver",
|
ParameterTypeResolver = "parameterTypeResolver",
|
||||||
FormLayoutProvider = "formLayoutProvider",
|
FormLayoutProvider = "formLayoutProvider",
|
||||||
|
StorageAccountService = "storageAccount",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrowserEnvironment
|
export interface BrowserEnvironment
|
||||||
|
@ -42,6 +44,7 @@ export interface BrowserEnvironmentConfig extends EnvironmentConfig {
|
||||||
export interface BrowserDependencyFactories extends DependencyFactories {
|
export interface BrowserDependencyFactories extends DependencyFactories {
|
||||||
[BrowserDependencyName.ParameterTypeResolver]: () => ParameterTypeResolver;
|
[BrowserDependencyName.ParameterTypeResolver]: () => ParameterTypeResolver;
|
||||||
[BrowserDependencyName.FormLayoutProvider]: () => FormLayoutProvider;
|
[BrowserDependencyName.FormLayoutProvider]: () => FormLayoutProvider;
|
||||||
|
[BrowserDependencyName.StorageAccountService]: () => StorageAccountService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
mockDependencyFactories,
|
mockDependencyFactories,
|
||||||
mockEnvironmentConfig,
|
mockEnvironmentConfig,
|
||||||
} from "@batch/ui-common/lib/environment";
|
} from "@batch/ui-common/lib/environment";
|
||||||
|
import { FakeStorageAccountService } from "@batch/ui-service";
|
||||||
import { initializeIcons } from "@fluentui/react/lib/Icons";
|
import { initializeIcons } from "@fluentui/react/lib/Icons";
|
||||||
import { BrowserEnvironmentConfig } from ".";
|
import { BrowserEnvironmentConfig } from ".";
|
||||||
import {
|
import {
|
||||||
|
@ -17,12 +18,9 @@ import { MockBrowserEnvironment } from "./mock-browser-environment";
|
||||||
let _fluentIconsInitialized = false;
|
let _fluentIconsInitialized = false;
|
||||||
|
|
||||||
export const mockBrowserDepFactories: Partial<BrowserDependencyFactories> = {
|
export const mockBrowserDepFactories: Partial<BrowserDependencyFactories> = {
|
||||||
parameterTypeResolver: () => {
|
parameterTypeResolver: () => new DefaultParameterTypeResolver(),
|
||||||
return new DefaultParameterTypeResolver();
|
formLayoutProvider: () => new DefaultFormLayoutProvider(),
|
||||||
},
|
storageAccount: () => new FakeStorageAccountService(),
|
||||||
formLayoutProvider: () => {
|
|
||||||
return new DefaultFormLayoutProvider();
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface ArmResourceListResponse<T> {
|
||||||
|
value: T[];
|
||||||
|
nextLink?: string;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
export const Endpoints = {
|
||||||
|
arm: `https://management.azure.com`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ApiVersion = {
|
||||||
|
arm: `2022-05-01`,
|
||||||
|
batch: {
|
||||||
|
arm: `2022-06-01`,
|
||||||
|
data: `2022-06-01`,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
arm: `2022-05-01`,
|
||||||
|
data: `2022-05-01`,
|
||||||
|
},
|
||||||
|
};
|
|
@ -1,3 +1,5 @@
|
||||||
export * from "./http-service";
|
export * from "./http-service";
|
||||||
export * from "./view";
|
export * from "./view";
|
||||||
export * from "./certificate";
|
export * from "./certificate";
|
||||||
|
export * from "./storage";
|
||||||
|
export * from "./subscription";
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import {
|
||||||
|
initMockEnvironment,
|
||||||
|
getMockEnvironment,
|
||||||
|
} from "@batch/ui-common/lib/environment";
|
||||||
|
import { MockHttpClient, MockHttpResponse } from "@batch/ui-common/lib/http";
|
||||||
|
import { StorageAccountServiceImpl } from "..";
|
||||||
|
import { ApiVersion, Endpoints } from "../../constants";
|
||||||
|
|
||||||
|
describe("StorageAccountService", () => {
|
||||||
|
let httpClient: MockHttpClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
initMockEnvironment();
|
||||||
|
httpClient = getMockEnvironment().getHttpClient();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
const assertions = httpClient.remainingAssertions();
|
||||||
|
if (assertions.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`HTTP client has untested assertions (${assertions.join(", ")})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("list()", async () => {
|
||||||
|
const service = new StorageAccountServiceImpl();
|
||||||
|
|
||||||
|
httpClient.addExpected(
|
||||||
|
new MockHttpResponse(
|
||||||
|
`${Endpoints.arm}/subscriptions//fake/sub1/providers/Microsoft.Storage/storageAccounts?api-version=${ApiVersion.storage.arm}`,
|
||||||
|
200,
|
||||||
|
JSON.stringify({
|
||||||
|
value: [
|
||||||
|
{ id: "1", name: "One" },
|
||||||
|
{ id: "2", name: "Two" },
|
||||||
|
{ id: "3", name: "Three" },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const accounts = await service.list("/fake/sub1");
|
||||||
|
|
||||||
|
expect(accounts.length).toEqual(3);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { StorageAccount } from "./storage-account-models";
|
||||||
|
import { StorageAccountService } from "./storage-account-service";
|
||||||
|
|
||||||
|
const subscriptionAccounts: { [key: string]: StorageAccount[] } = {
|
||||||
|
"/fake/sub1": [
|
||||||
|
{ id: "/fake/storageA", name: "Storage A" },
|
||||||
|
{ id: "/fake/storageB", name: "Storage B" },
|
||||||
|
{ id: "/fake/storageC", name: "Storage C" },
|
||||||
|
],
|
||||||
|
"/fake/sub2": [
|
||||||
|
{ id: "/fake/storageD", name: "Storage D" },
|
||||||
|
{ id: "/fake/storageE", name: "Storage E" },
|
||||||
|
],
|
||||||
|
"/fake/sub3": [
|
||||||
|
{ id: "/fake/storageF", name: "Storage F" },
|
||||||
|
{ id: "/fake/storageG", name: "Storage G" },
|
||||||
|
{ id: "/fake/storageH", name: "Storage H" },
|
||||||
|
{ id: "/fake/storageI", name: "Storage I" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FakeStorageAccountService implements StorageAccountService {
|
||||||
|
public async list(subscriptionId: string): Promise<StorageAccount[]> {
|
||||||
|
if (subscriptionId in subscriptionAccounts) {
|
||||||
|
return subscriptionAccounts[subscriptionId];
|
||||||
|
} else if (subscriptionId === "/fake/badsub") {
|
||||||
|
// Simulates a network error.
|
||||||
|
throw new Error("No storage accounts in subscription.");
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(): Promise<StorageAccount | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
public async remove(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
public async update(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./storage-account-service";
|
||||||
|
export * from "./storage-account-models";
|
||||||
|
export * from "./fake-storage-account-service";
|
|
@ -0,0 +1,4 @@
|
||||||
|
export interface StorageAccount {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { ApiVersion, Endpoints } from "../constants";
|
||||||
|
import { AbstractHttpService } from "../http-service";
|
||||||
|
import { ArmResourceListResponse } from "../arm";
|
||||||
|
import { StorageAccount } from "./storage-account-models";
|
||||||
|
|
||||||
|
export interface StorageAccountService {
|
||||||
|
list(subscriptionId: string): Promise<StorageAccount[]>;
|
||||||
|
get(id: string): Promise<StorageAccount | null>;
|
||||||
|
create(account: StorageAccount): Promise<void>;
|
||||||
|
remove(account: StorageAccount): Promise<void>;
|
||||||
|
update(account: StorageAccount): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StorageAccountServiceImpl
|
||||||
|
extends AbstractHttpService
|
||||||
|
implements StorageAccountService
|
||||||
|
{
|
||||||
|
public async list(subscriptionId: string): Promise<StorageAccount[]> {
|
||||||
|
const response = await this.httpClient.get(
|
||||||
|
`${Endpoints.arm}/subscriptions/${subscriptionId}/providers/Microsoft.Storage/storageAccounts?api-version=${ApiVersion.storage.arm}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const json =
|
||||||
|
(await response.json()) as ArmResourceListResponse<StorageAccount>;
|
||||||
|
return json.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get(): Promise<StorageAccount | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async create(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
public async remove(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
public async update(): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./subscription-models";
|
|
@ -0,0 +1,7 @@
|
||||||
|
export interface Subscription {
|
||||||
|
id: string;
|
||||||
|
subscriptionId: string;
|
||||||
|
tenantId: string;
|
||||||
|
displayName: string;
|
||||||
|
state: string;
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import {
|
||||||
DefaultParameterTypeResolver,
|
DefaultParameterTypeResolver,
|
||||||
} from "@batch/ui-react/lib/components/form";
|
} from "@batch/ui-react/lib/components/form";
|
||||||
import { StandardLocalizer } from "@batch/ui-common/lib/localization";
|
import { StandardLocalizer } from "@batch/ui-common/lib/localization";
|
||||||
|
import { FakeStorageAccountService } from "@batch/ui-service";
|
||||||
|
|
||||||
// Defined by webpack
|
// Defined by webpack
|
||||||
declare const ENV: {
|
declare const ENV: {
|
||||||
|
@ -30,6 +31,8 @@ export function init(rootEl: HTMLElement): void {
|
||||||
[DependencyName.Logger]: () => new ConsoleLogger(),
|
[DependencyName.Logger]: () => new ConsoleLogger(),
|
||||||
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
[DependencyName.Localizer]: () => new StandardLocalizer(),
|
||||||
[DependencyName.HttpClient]: () => new MockHttpClient(),
|
[DependencyName.HttpClient]: () => new MockHttpClient(),
|
||||||
|
[BrowserDependencyName.StorageAccountService]: () =>
|
||||||
|
new FakeStorageAccountService(),
|
||||||
[BrowserDependencyName.ParameterTypeResolver]: () =>
|
[BrowserDependencyName.ParameterTypeResolver]: () =>
|
||||||
new DefaultParameterTypeResolver(),
|
new DefaultParameterTypeResolver(),
|
||||||
[BrowserDependencyName.FormLayoutProvider]: () =>
|
[BrowserDependencyName.FormLayoutProvider]: () =>
|
||||||
|
|
Загрузка…
Ссылка в новой задаче