Resolved PR comments

Added tests
Moved onSubmt and initialize inside base class
Moved testExplorer to separate folder
made fields of SelfServe Class non static
This commit is contained in:
Srinath Narayanan 2021-01-12 22:02:45 -08:00
parent 373327dc88
commit 318842624f
32 changed files with 1254 additions and 905 deletions

View File

@ -1,3 +1,4 @@
module.exports = {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"]
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-react", "@babel/preset-typescript"],
plugins: [["@babel/plugin-proposal-decorators", { legacy: true }]]
};

28
package-lock.json generated
View File

@ -393,7 +393,6 @@
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz",
"integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.10.4",
"@babel/helper-member-expression-to-functions": "^7.12.1",
@ -620,6 +619,25 @@
"@babel/plugin-syntax-async-generators": "^7.8.0"
}
},
"@babel/plugin-proposal-class-properties": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz",
"integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==",
"requires": {
"@babel/helper-create-class-features-plugin": "^7.12.1",
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/plugin-proposal-decorators": {
"version": "7.12.12",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.12.tgz",
"integrity": "sha512-fhkE9lJYpw2mjHelBpM2zCbaA11aov2GJs7q4cFaXNrWx0H3bW58H9Esy2rdtYOghFBEYUDRIpvlgi+ZD+AvvQ==",
"requires": {
"@babel/helper-create-class-features-plugin": "^7.12.1",
"@babel/helper-plugin-utils": "^7.10.4",
"@babel/plugin-syntax-decorators": "^7.12.1"
}
},
"@babel/plugin-proposal-dynamic-import": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz",
@ -729,6 +747,14 @@
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/plugin-syntax-decorators": {
"version": "7.12.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz",
"integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==",
"requires": {
"@babel/helper-plugin-utils": "^7.10.4"
}
},
"@babel/plugin-syntax-dynamic-import": {
"version": "7.8.3",
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz",

View File

@ -8,6 +8,8 @@
"@azure/cosmos": "3.9.0",
"@azure/cosmos-language-service": "0.0.5",
"@azure/identity": "1.1.0",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-decorators": "7.12.12",
"@jupyterlab/services": "6.0.0-rc.2",
"@jupyterlab/terminal": "3.0.0-rc.2",
"@microsoft/applicationinsights-web": "2.5.9",

View File

@ -15,7 +15,7 @@ import * as ReactDOM from "react-dom";
export interface ReactAdapter {
parameters: any;
renderComponent: () => JSX.Element;
renderComponent: (() => Promise<JSX.Element>) | (() => JSX.Element);
setElement?: (elt: Element) => void;
}
@ -36,12 +36,12 @@ export class Registerer {
}
// If any of the ko observable change inside parameters, trigger a new render.
ko.computed(() => ko.toJSON(adapter.parameters)).subscribe(() =>
ReactDOM.render(adapter.renderComponent(), element)
ko.computed(() => ko.toJSON(adapter.parameters)).subscribe(async () =>
ReactDOM.render(await adapter.renderComponent(), element)
);
// Initial rendering at mount point
ReactDOM.render(adapter.renderComponent(), element);
Promise.resolve(adapter.renderComponent()).then(component => ReactDOM.render(component, element));
}
} as ko.BindingHandler;
}

View File

@ -48,7 +48,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{ key: "feature.selfServeTypeForTest", label: "self serve type passed on for testing", value: "sample" },
{ key: "feature.selfServeTypeForTest", label: "Self serve type passed on for testing", value: "sample" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",

View File

@ -157,14 +157,14 @@ exports[`Feature panel renders all flags 1`] = `
/>
<StyledCheckboxBase
checked={false}
key="feature.enableLinkInjection"
label="Enable Injecting Notebook Viewer Link into the first cell"
key="feature.selfServeTypeForTest"
label="Self serve type passed on for testing"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
key="feature.enableLinkInjection"
label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]}
/>
</Stack>
@ -172,6 +172,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow"
horizontalAlign="space-between"
>
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase
checked={false}
key="feature.enablefixedcollectionwithsharedthroughput"

View File

@ -1118,6 +1118,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@ -2393,6 +2401,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@ -3681,6 +3697,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],
@ -4956,6 +4980,14 @@ exports[`SettingsComponent renders 1`] = `
},
"selectedDatabaseId": [Function],
"selectedNode": [Function],
"selfServeComponentAdapter": SelfServeComponentAdapter {
"container": [Circular],
"parameters": [Function],
},
"selfServeLoadingComponentAdapter": SelfServeLoadingComponentAdapter {
"parameters": [Function],
},
"selfServeType": [Function],
"serverId": [Function],
"settingsPane": SettingsPane {
"container": [Circular],

View File

@ -1,15 +1,25 @@
import React from "react";
import { shallow } from "enzyme";
import { SmartUiComponent, Descriptor } from "./SmartUiComponent";
import { SmartUiComponent, Descriptor, UiType } from "./SmartUiComponent";
describe("SmartUiComponent", () => {
let initializeCalled = false;
let fetchMaxCalled = false;
const initializeMock = async () => {
initializeCalled = true;
return new Map();
};
const fetchMaxvalue = async () => {
fetchMaxCalled = true;
return 500;
};
const exampleData: Descriptor = {
onSubmit: async () => {
return;
},
initialize: async () => {
return undefined;
},
initialize: initializeMock,
root: {
id: "root",
info: {
@ -27,11 +37,10 @@ describe("SmartUiComponent", () => {
dataFieldName: "throughput",
type: "number",
min: 400,
max: 500,
max: fetchMaxvalue,
step: 10,
defaultValue: 400,
inputType: "spinner",
onChange: undefined
uiType: UiType.Spinner
}
},
{
@ -44,8 +53,21 @@ describe("SmartUiComponent", () => {
max: 500,
step: 10,
defaultValue: 400,
inputType: "slider",
onChange: undefined
uiType: UiType.Slider
}
},
{
id: "throughput3",
input: {
label: "Throughput (invalid)",
dataFieldName: "throughput3",
type: "boolean",
min: 400,
max: 500,
step: 10,
defaultValue: 400,
uiType: UiType.Spinner,
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'"
}
},
{
@ -53,8 +75,7 @@ describe("SmartUiComponent", () => {
input: {
label: "Container id",
dataFieldName: "containerId",
type: "string",
onChange: undefined
type: "string"
}
},
{
@ -65,8 +86,7 @@ describe("SmartUiComponent", () => {
falseLabel: "Disabled",
defaultValue: true,
dataFieldName: "analyticalStore",
type: "boolean",
onChange: undefined
type: "boolean"
}
},
{
@ -80,7 +100,6 @@ describe("SmartUiComponent", () => {
{ label: "Database 2", key: "db2" },
{ label: "Database 3", key: "db3" }
],
onChange: undefined,
defaultKey: "db2"
}
}
@ -88,8 +107,17 @@ describe("SmartUiComponent", () => {
}
};
it("should render", () => {
it("should render", done => {
const wrapper = shallow(<SmartUiComponent descriptor={exampleData} />);
expect(wrapper).toMatchSnapshot();
setImmediate(() => {
expect(wrapper).toMatchSnapshot();
expect(initializeCalled).toBeTruthy();
expect(fetchMaxCalled).toBeTruthy();
wrapper.setState({ isRefreshing: true });
wrapper.update();
expect(wrapper).toMatchSnapshot();
done();
});
});
});

View File

@ -22,12 +22,15 @@ import "./SmartUiComponent.less";
export type InputTypeValue = "number" | "string" | "boolean" | "object";
export type NumberInputType = "spinner" | "slider";
export enum UiType {
Spinner = "Spinner",
Slider = "Slider"
}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type ChoiceItem = { label: string; key: string };
export type DropdownItem = { label: string; key: string };
export type InputType = number | string | boolean | ChoiceItem | JSX.Element;
export type InputType = number | string | boolean | DropdownItem | JSX.Element;
export interface BaseInput {
label: (() => Promise<string>) | string;
@ -35,7 +38,7 @@ export interface BaseInput {
type: InputTypeValue;
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
placeholder?: (() => Promise<string>) | string;
customElement?: ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) | JSX.Element;
errorMessage?: string;
}
/**
@ -46,7 +49,7 @@ export interface NumberInput extends BaseInput {
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number;
defaultValue?: number;
inputType: NumberInputType;
uiType: UiType;
}
export interface BooleanInput extends BaseInput {
@ -59,8 +62,8 @@ export interface StringInput extends BaseInput {
defaultValue?: string;
}
export interface ChoiceInput extends BaseInput {
choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
export interface DropdownInput extends BaseInput {
choices: (() => Promise<DropdownItem[]>) | DropdownItem[];
defaultKey?: string;
}
@ -72,7 +75,7 @@ export interface Info {
};
}
export type AnyInput = NumberInput | BooleanInput | StringInput | ChoiceInput;
export type AnyInput = NumberInput | BooleanInput | StringInput | DropdownInput;
export interface Node {
id: string;
@ -83,8 +86,9 @@ export interface Node {
export interface Descriptor {
root: Node;
initialize: () => Promise<Map<string, InputType>>;
onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
initialize?: () => Promise<Map<string, InputType>>;
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
inputNames?: string[];
}
/************************** Component implementation starts here ************************************* */
@ -97,56 +101,30 @@ interface SmartUiComponentState {
currentValues: Map<string, InputType>;
baselineValues: Map<string, InputType>;
errors: Map<string, string>;
customInputIndex: number;
isRefreshing: boolean;
}
export class SmartUiComponent extends React.Component<SmartUiComponentProps, SmartUiComponentState> {
private customInputs: AnyInput[] = [];
private shouldRenderCustomComponents = true;
private static readonly labelStyle = {
color: "#393939",
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
fontSize: 12
};
componentDidMount(): void {
this.setDefaultValues();
}
constructor(props: SmartUiComponentProps) {
super(props);
this.state = {
baselineValues: new Map(),
currentValues: new Map(),
errors: new Map(),
customInputIndex: 0,
isRefreshing: false
};
this.setDefaultValues();
}
componentDidUpdate = async (): Promise<void> => {
if (!this.customInputs.length) {
return;
}
if (!this.shouldRenderCustomComponents) {
this.shouldRenderCustomComponents = true;
return;
}
if (this.state.customInputIndex === this.customInputs.length) {
this.shouldRenderCustomComponents = false;
this.setState({ customInputIndex: 0 });
return;
}
const input = this.customInputs[this.state.customInputIndex];
const dataFieldName = input.dataFieldName;
const element = await (input.customElement as Function)(this.state.currentValues);
const { currentValues } = this.state;
currentValues.set(dataFieldName, element);
this.setState({ currentValues: currentValues, customInputIndex: this.state.customInputIndex + 1 });
};
private setDefaultValues = async (): Promise<void> => {
this.setState({ isRefreshing: true });
await this.setDefaults(this.props.descriptor.root);
@ -159,6 +137,11 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
let { currentValues, baselineValues } = this.state;
const initialValues = await this.props.descriptor.initialize();
for (const key of initialValues.keys()) {
if (this.props.descriptor.inputNames.indexOf(key) === -1) {
console.log(this.props.descriptor.inputNames);
this.setState({ isRefreshing: false });
throw new Error(`${key} is not an input property of this class.`);
}
currentValues = currentValues.set(key, initialValues.get(key));
baselineValues = baselineValues.set(key, initialValues.get(key));
}
@ -182,8 +165,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
if (currentNode.input) {
currentNode.input = await this.getModifiedInput(currentNode.input);
}
await Promise.all(currentNode.children?.map(async (child: Node) => await this.setDefaults(child)));
const promises = currentNode.children?.map(async (child: Node) => await this.setDefaults(child));
if (promises) {
await Promise.all(promises);
}
};
private getModifiedInput = async (input: AnyInput): Promise<AnyInput> => {
@ -195,13 +180,6 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
input.placeholder = await (input.placeholder as Function)();
}
if (input.customElement) {
if (input.customElement instanceof Function) {
this.customInputs.push(input);
}
return input;
}
switch (input.type) {
case "string": {
return input as StringInput;
@ -230,7 +208,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return booleanInput;
}
default: {
const enumInput = input as ChoiceInput;
const enumInput = input as DropdownInput;
if (enumInput.choices instanceof Function) {
enumInput.choices = await (enumInput.choices as Function)();
}
@ -252,7 +230,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
return (input as BooleanInput).defaultValue as boolean;
}
default: {
return (input as ChoiceInput).defaultKey as string;
return (input as DropdownInput).defaultKey as string;
}
}
};
@ -364,7 +342,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
};
const value = this.state.currentValues.get(dataFieldName) as number;
if (input.inputType === "spinner") {
if (input.uiType === UiType.Spinner) {
return (
<div>
<SpinButton
@ -386,7 +364,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
)}
</div>
);
} else if (input.inputType === "slider") {
} else if (input.uiType === UiType.Slider) {
return (
<Slider
{...props}
@ -402,7 +380,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
/>
);
} else {
return <>Unsupported number input type {input.inputType}</>;
return <>Unsupported number UI type {input.uiType}</>;
}
}
@ -440,7 +418,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
);
}
private renderEnumInput(input: ChoiceInput): JSX.Element {
private renderEnumInput(input: DropdownInput): JSX.Element {
const { label, defaultKey: defaultKey, dataFieldName, choices, placeholder } = input;
return (
<Dropdown
@ -452,7 +430,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
}
onChange={(_, item: IDropdownOption) => this.onInputChange(input, item.key.toString())}
placeholder={placeholder as string}
options={(choices as ChoiceItem[]).map(c => ({
options={(choices as DropdownItem[]).map(c => ({
key: c.key,
text: c.label
}))}
@ -467,19 +445,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
);
}
private renderCustomInput(input: AnyInput): JSX.Element {
if (input.customElement instanceof Function) {
const dataFieldName = input.dataFieldName;
const element = this.state.currentValues.get(dataFieldName) as JSX.Element;
return element ? element : <></>;
} else {
return input.customElement as JSX.Element;
}
private renderError(input: AnyInput): JSX.Element {
return <MessageBar messageBarType={MessageBarType.error}>Error: {input.errorMessage}</MessageBar>;
}
private renderInput(input: AnyInput): JSX.Element {
if (input.customElement) {
return this.renderCustomInput(input);
if (input.errorMessage) {
return this.renderError(input);
}
switch (input.type) {
case "string":
@ -489,7 +461,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
case "boolean":
return this.renderBooleanInput(input as BooleanInput);
default:
return this.renderEnumInput(input as ChoiceInput);
return this.renderEnumInput(input as DropdownInput);
}
}
@ -509,7 +481,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
render(): JSX.Element {
const containerStackTokens: IStackTokens = { childrenGap: 20 };
return this.state.currentValues && this.state.currentValues.size && !this.state.isRefreshing ? (
return !this.state.isRefreshing ? (
<div style={{ overflowX: "auto" }}>
<Stack tokens={containerStackTokens} styles={{ root: { width: 400, padding: 10 } }}>
{this.renderNode(this.props.descriptor.root)}

View File

@ -1,240 +1,340 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SmartUiComponent should render 1`] = `
<Fragment>
<div
style={
Object {
"overflowX": "auto",
}
}
>
<Stack
className="widgetRendererContainer"
styles={
Object {
"root": Object {
"padding": 10,
"width": 400,
},
}
}
tokens={
Object {
"childrenGap": 10,
"childrenGap": 20,
}
}
>
<StyledMessageBarBase>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
<div
key="throughput"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
>
<div>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
defaultValue="400"
disabled={false}
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label="Throughput (input)"
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
styles={
Object {
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/>
</div>
</Stack>
</div>
<div
key="throughput2"
}
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
<StackItem>
<StyledMessageBarBase>
Start at $24/mo per database
<StyledLinkBase
href="https://aka.ms/azure-cosmos-db-pricing"
target="_blank"
>
More Details
</StyledLinkBase>
</StyledMessageBarBase>
</StackItem>
<div
key="throughput"
>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
defaultValue={400}
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
<Stack
className="widgetRendererContainer"
tokens={
Object {
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"childrenGap": 15,
}
}
/>
</Stack>
</div>
<div
key="containerId"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<div
className="stringInputContainer"
>
<div>
<StyledTextFieldBase
id="containerId-input"
label="Container id"
<StackItem>
<div>
<CustomizedSpinButton
ariaLabel="Throughput (input)"
decrementButtonIcon={
Object {
"iconName": "ChevronDownSmall",
}
}
disabled={false}
incrementButtonIcon={
Object {
"iconName": "ChevronUpSmall",
}
}
label="Throughput (input)"
labelPosition={0}
max={500}
min={400}
onDecrement={[Function]}
onIncrement={[Function]}
onValidate={[Function]}
step={10}
styles={
Object {
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="throughput2"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<StyledSliderBase
ariaLabel="Throughput (Slider)"
label="Throughput (Slider)"
max={500}
min={400}
onChange={[Function]}
step={10}
styles={
Object {
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
"titleLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"valueLabel": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
}
}
type="text"
/>
</div>
</div>
</Stack>
</div>
<div
key="analyticalStore"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
</StackItem>
</Stack>
</div>
<div
key="throughput3"
>
<div>
<div
className="inputLabelContainer"
>
<Text
className="inputLabel"
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
</Stack>
</div>
<div
key="database"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 10,
}
}
>
<StyledWithResponsiveMode
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "database1",
},
Object {
"key": "db2",
"text": "database2",
},
Object {
"key": "db3",
"text": "database3",
},
]
}
selectedKey="db2"
styles={
<Stack
className="widgetRendererContainer"
tokens={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
"childrenGap": 15,
}
}
/>
</Stack>
</div>
>
<StackItem>
<StyledMessageBarBase
messageBarType={1}
>
Error:
label, truelabel and falselabel are required for boolean input 'throughput3'
</StyledMessageBarBase>
</StackItem>
</Stack>
</div>
<div
key="containerId"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<div
className="stringInputContainer"
>
<div>
<StyledTextFieldBase
id="containerId-input"
label="Container id"
onChange={[Function]}
styles={
Object {
"subComponentStyles": Object {
"label": Object {
"root": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
},
},
}
}
type="text"
/>
</div>
</div>
</StackItem>
</Stack>
</div>
<div
key="analyticalStore"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<div>
<div
className="inputLabelContainer"
>
<Text
className="inputLabel"
nowrap={true}
variant="small"
>
Analytical Store
</Text>
</div>
<RadioSwitchComponent
choices={
Array [
Object {
"key": "false",
"label": "Disabled",
"onSelect": [Function],
},
Object {
"key": "true",
"label": "Enabled",
"onSelect": [Function],
},
]
}
selectedKey="true"
/>
</div>
</StackItem>
</Stack>
</div>
<div
key="database"
>
<Stack
className="widgetRendererContainer"
tokens={
Object {
"childrenGap": 15,
}
}
>
<StackItem>
<StyledWithResponsiveMode
label="Database"
onChange={[Function]}
options={
Array [
Object {
"key": "db1",
"text": "Database 1",
},
Object {
"key": "db2",
"text": "Database 2",
},
Object {
"key": "db3",
"text": "Database 3",
},
]
}
selectedKey="db2"
styles={
Object {
"dropdown": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
},
"label": Object {
"color": "#393939",
"fontFamily": "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
"fontSize": 12,
"fontWeight": 600,
},
}
}
/>
</StackItem>
</Stack>
</div>
</Stack>
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 10,
}
}
>
<CustomizedPrimaryButton
onClick={[Function]}
styles={
Object {
"root": Object {
"width": 100,
},
}
}
text="submit"
/>
<CustomizedPrimaryButton
onClick={[Function]}
styles={
Object {
"root": Object {
"width": 100,
},
}
}
text="discard"
/>
</Stack>
</Stack>
</Fragment>
</div>
`;
exports[`SmartUiComponent should render 2`] = `
<StyledSpinnerBase
size={3}
styles={
Object {
"root": Object {
"height": "100%",
"justifyContent": "center",
"textAlign": "center",
"width": "100%",
},
}
}
/>
`;

View File

@ -1857,8 +1857,6 @@ export default class Explorer {
this.selfServeType(inputs.selfServeType);
} else {
this.selfServeType(SelfServeTypes.none);
this._setLoadingStatusText("Connecting...", "Welcome to Azure Cosmos DB");
this._setConnectingImage();
}
}
@ -2994,11 +2992,6 @@ export default class Explorer {
}
}
private _setConnectingImage() {
const connectingImage = document.getElementById("explorerConnectingImage");
connectingImage.innerHTML = '<img src="../images/HdeConnectCosmosDB.svg" >';
}
private _openSetupNotebooksPaneForQuickstart(): void {
const title = "Enable Notebooks (Preview)";
const description =

View File

@ -78,6 +78,7 @@ import hdeConnectImage from "../images/HdeConnectCosmosDB.svg";
import refreshImg from "../images/refresh-cosmos.svg";
import arrowLeftImg from "../images/imgarrowlefticon.svg";
import { KOCommentEnd, KOCommentIfStart } from "./koComment";
import { Spinner, SpinnerSize } from "office-ui-fabric-react";
// TODO: Encapsulate and reuse all global variables as environment variables
window.authType = AuthType.AAD;
@ -131,9 +132,14 @@ const App: React.FunctionComponent = () => {
className="flexContainer"
data-bind="visible: selfServeType() && selfServeType() !== 'none', react: selfServeComponentAdapter"
></div>
<div id="divExplorer" className="flexContainer hideOverflows" style={{ display: "none" }}>
<div
id="divExplorer"
data-bind="if: selfServeType() === 'none'"
className="flexContainer hideOverflows"
style={{ display: "none" }}
>
{/* Main Command Bar - Start */}
<div data-bind="visible: selfServeType() === 'none', react: commandBarComponentAdapter" />
<div data-bind="react: commandBarComponentAdapter" />
{/* Main Command Bar - End */}
{/* Share url flyout - Start */}
<div
@ -201,7 +207,7 @@ const App: React.FunctionComponent = () => {
</div>
{/* Share url flyout - End */}
{/* Collections Tree and Tabs - Begin */}
<div className="resourceTreeAndTabs" data-bind="visible: selfServeType() === 'none'">
<div className="resourceTreeAndTabs">
{/* Collections Tree - Start */}
<div id="resourcetree" data-test="resourceTreeId" className="resourceTree">
<div className="collectionsTreeWithSplitter">
@ -308,10 +314,7 @@ const App: React.FunctionComponent = () => {
data-bind="visible: !isRefreshingExplorer() && tabsManager.openedTabs().length === 0"
>
<form className="connectExplorerFormContainer">
<div
className="connectExplorer"
data-bind="visible: selfServeType() === 'none', react: splashScreenAdapter"
/>
<div className="connectExplorer" data-bind="react: splashScreenAdapter" />
</form>
</div>
<div
@ -325,7 +328,7 @@ const App: React.FunctionComponent = () => {
role="contentinfo"
aria-label="Notification console"
id="explorerNotificationConsole"
data-bind="react: notificationConsoleComponentAdapter, visible: selfServeType() === 'none'"
data-bind="react: notificationConsoleComponentAdapter"
/>
</div>
{/* Explorer Connection - Encryption Token / AAD - Start */}
@ -379,25 +382,20 @@ const App: React.FunctionComponent = () => {
</div>
{/* Explorer Connection - Encryption Token / AAD - End */}
{/* Global loader - Start */}
{window.dataExplorer && <Spinner size={SpinnerSize.large} />}
<div className="splashLoaderContainer" data-bind="visible: isRefreshingExplorer">
<div className="splashLoaderContentContainer">
<div data-bind="visible: selfServeType() === undefined, react: selfServeLoadingComponentAdapter"></div>
<p
className="connectExplorerContent"
id="explorerConnectingImage"
data-bind="visible: selfServeType() === 'none'"
></p>
<p
className="splashLoaderTitle"
id="explorerLoadingStatusTitle"
data-bind="visible: selfServeType() === 'none'"
></p>
<p
className="splashLoaderText"
id="explorerLoadingStatusText"
role="alert"
data-bind="visible: selfServeType() === 'none'"
></p>
<div data-bind="if: selfServeType() === 'none'" style={{ display: "none" }}>
<p className="connectExplorerContent">
<img src={hdeConnectImage} alt="Azure Cosmos DB" />
</p>
<p className="splashLoaderTitle">Welcome to Azure Cosmos DB</p>
<p className="splashLoaderText" role="alert">
Connecting...
</p>
</div>
</div>
</div>
{/* Global loader - End */}

View File

@ -0,0 +1,14 @@
import { Info } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { addPropertyToMap, buildSmartUiDescriptor } from "./SelfServeUtils";
export const IsDisplayable = (): ClassDecorator => {
return (target: Function) => {
buildSmartUiDescriptor(target.name, target.prototype);
};
};
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
return (target: Function) => {
addPropertyToMap(target.prototype, "root", target.name, "info", info);
};
};

View File

@ -1,26 +0,0 @@
import { Info, InputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { addPropertyToMap, toSmartUiDescriptor } from "./SelfServeUtils";
export const SmartUi = (): ClassDecorator => {
return (target: Function) => {
toSmartUiDescriptor(target.name, target);
};
};
export const ClassInfo = (info: (() => Promise<Info>) | Info): ClassDecorator => {
return (target: Function) => {
addPropertyToMap(target, "root", target.name, "info", info);
};
};
export const OnSubmit = (onSubmit: (currentValues: Map<string, InputType>) => Promise<void>): ClassDecorator => {
return (target: Function) => {
addPropertyToMap(target, "root", target.name, "onSubmit", onSubmit);
};
};
export const Initialize = (initialize: () => Promise<Map<string, InputType>>): ClassDecorator => {
return (target: Function) => {
addPropertyToMap(target, "root", target.name, "initialize", initialize);
};
};

View File

@ -1,30 +0,0 @@
import React from "react";
import { HoverCard, HoverCardType, Stack, Text } from "office-ui-fabric-react";
import { InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
interface TextComponentProps {
text: string;
currentValues: Map<string, InputType>;
}
export class TextComponent extends React.Component<TextComponentProps> {
private onHover = (): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 5, padding: 15 }}>
<Text>Choice: {this.props.currentValues.get("choiceInput")?.toString()}</Text>
<Text>Boolean: {this.props.currentValues.get("booleanInput")?.toString()}</Text>
<Text>String: {this.props.currentValues.get("stringInput")?.toString()}</Text>
<Text>Slider: {this.props.currentValues.get("numberSliderInput")?.toString()}</Text>
<Text>Spinner: {this.props.currentValues.get("numberSpinnerInput")?.toString()}</Text>
</Stack>
);
};
public render(): JSX.Element {
return (
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
<Text styles={{ root: { fontWeight: 600 } }}>{this.props.text}</Text>
</HoverCard>
);
}
}

View File

@ -1,189 +0,0 @@
import {
Label,
ParentOf,
PropertyInfo,
OnChange,
Placeholder,
CustomElement,
ChoiceInput,
BooleanInput,
NumberInput
} from "../PropertyDescriptors";
import { SmartUi, ClassInfo, OnSubmit, Initialize } from "../ClassDescriptors";
import {
initializeSelfServeExample,
choiceInfo,
choiceOptions,
onSliderChange,
onSubmit,
renderText,
selfServeExampleInfo,
descriptionElement,
initializeNumberMaxValue
} from "./ExampleApis";
import { SelfServeBase } from "../SelfServeUtils";
import { ChoiceItem } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
/*
This is an example self serve class that auto generates UI components for your feature.
Each self serve class
- Needs to extends the SelfServeBase class.
- Needs to have the @SmartUi() descriptor to tell the compiler that UI needs to be generated from this class.
- Needs to have an @OnSubmit() descriptor, a callback for when the submit button is clicked.
- Needs to have an @Initialize() descriptor, to set default values for the inputs.
You can test this self serve UI by using the featureflag '?feature.selfServeTypeForTest=example'
and plumb in similar feature flags for your own self serve class.
The default values and functions used for this class can be found in ExampleApis.tsx
*/
/*
@SmartUi()
- role: Generated the JSON required to convert this class into the required UI. This is done during compile time.
*/
@SmartUi()
/*
@OnSubmit()
- input: (currentValues: Map<string, InputType>) => Promise<void>
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
calls here using the data from the different inputs passed as a Map to this callback function.
In this example, the onSubmit callback simply sets the value for keys corresponding to the field name
in the SessionStorage.
*/
@OnSubmit(onSubmit)
/*
@ClassInfo()
- input: Info | () => Promise<Info>
- role: Display an Info bar as the first element of the UI.
*/
@ClassInfo(selfServeExampleInfo)
/*
@Initialize()
- input: () => Promise<Map<string, InputType>>
- role: Set default values for the properties of this class.
The static properties of this class (namely choiceInput, booleanInput, stringInput, numberSliderInput, numberSpinnerInput)
will each correspond to an UI element. Their values can be of 'InputType'. Their defaults can be set by setting
values in a Map corresponding to the field's name.
Typically, you can make rest calls in the async function passed to @Initialize() to fetch the initial values for
these fields. This is called after the onSubmit callback, to reinitialize the defaults.
In this example, the initializeSelfServeExample function simply reads the SessionStorage to fetch the default values
for these fields. These are then set when the changes are submitted.
*/
@Initialize(initializeSelfServeExample)
export class SelfServeExample extends SelfServeBase {
/*
@CustomElement()
- input: JSX.Element | (currentValues: Map<string, InputType> => Promise<JSX.Element>)
- role: Display a custom element by either passing the element itself, or by passing a function that takes the current values
and renders a Component / JSX.Element.
In this example, we first use a static JSX.Element to show a description text. We also declare a CustomComponent, that
takes a Map of propertyName -> value, as input. It uses this to display a Hoverable Card which shows a snapshot of
the current values.
*/
@CustomElement(descriptionElement)
static description: string;
/*
@ParentOf()
- input: string[]
- role: Determines which UI elements are the children of which UI element. An array containing the names of the child properties
is passsed. You need to make sure these children are declared in this Class as proeprties.
*/
@ParentOf(["choiceInput", "booleanInput", "stringInput", "numberSliderInput", "numberSpinnerInput"])
@CustomElement(renderText("Hover to see current values..."))
static currentValues: string;
/*
@Label()
- input: string | () => Promise<string>
- role: Adds a label for the UI element. This is ignored for a custom element but is required for all other properties.
*/
@Label("Choice")
/*
@PropertyInfo()
- input: Info | () => Promise<Info>
- role: Display an Info bar above the UI element for this property.
*/
@PropertyInfo(choiceInfo)
/*
@ChoiceInput()
- input: ChoiceItem[] | () => Promise<ChoiceItem[]>
- role: Display a dropdown with choices.
*/
@ChoiceInput(choiceOptions)
static choiceInput: ChoiceItem;
@Label("Boolean")
/*
@BooleanInput()
- input:
trueLabel : string | () => Promise<string>
falseLabel : string | () => Promise<string>
- role: Add a boolean input eith radio buttons for true and false values.
*/
@BooleanInput({
trueLabel: "allowed",
falseLabel: "not allowed"
})
static booleanInput: boolean;
@Label("String")
/*
@PlaceHolder()
- input: string | () => Promise<string>
- role: Adds a placeholder for the string input
*/
@Placeholder("instance name")
static stringInput: string;
@Label("Slider")
/*
@OnChange()
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property
changes its value in the UI. This can be used to change other input values based on some other input.
The new Map of propertyName -> value is returned.
In this example, the onSliderChange function sets the spinner input to the same value as the slider input
when the slider in moved in the UI.
*/
@OnChange(onSliderChange)
/*
@NumberInput()
- input:
min : number | () => Promise<number>
max : number | () => Promise<number>
step : number | () => Promise<number>
numberInputType : NumberInputType
- role: Display a numeric input as slider or a spinner. The Min, Max and step to increase by need to be provided as well.
In this example, the Max value is fetched via an async function. This is resolved every time the UI is reloaded.
*/
@NumberInput({
min: 1,
max: initializeNumberMaxValue,
step: 1,
numberInputType: "slider"
})
static numberSliderInput: number;
@Label("Spinner")
@NumberInput({
min: 1,
max: initializeNumberMaxValue,
step: 1,
numberInputType: "spinner"
})
static numberSpinnerInput: number;
}

View File

@ -1,70 +0,0 @@
import React from "react";
import { ChoiceItem, Info, InputType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
import { TextComponent } from "./CustomComponent";
import { SessionStorageUtility } from "../../Shared/StorageUtility";
import { Text } from "office-ui-fabric-react";
export enum Choices {
Choice1 = "Choice1",
Choice2 = "Choice2",
Choice3 = "Choice3"
}
export const choiceOptions: ChoiceItem[] = [
{ label: "Choice 1", key: Choices.Choice1 },
{ label: "Choice 2", key: Choices.Choice2 },
{ label: "Choice 3", key: Choices.Choice3 }
];
export const selfServeExampleInfo: Info = {
message: "This is a self serve class"
};
export const choiceInfo: Info = {
message: "More choices can be added in the future."
};
export const onSliderChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
currentState.set("numberSliderInput", newValue);
currentState.set("numberSpinnerInput", newValue);
return currentState;
};
export const onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
SessionStorageUtility.setEntry("choiceInput", currentValues.get("choiceInput")?.toString());
SessionStorageUtility.setEntry("booleanInput", currentValues.get("booleanInput")?.toString());
SessionStorageUtility.setEntry("stringInput", currentValues.get("stringInput")?.toString());
SessionStorageUtility.setEntry("numberSliderInput", currentValues.get("numberSliderInput")?.toString());
SessionStorageUtility.setEntry("numberSpinnerInput", currentValues.get("numberSpinnerInput")?.toString());
};
const delay = (ms: number): Promise<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
export const initializeSelfServeExample = async (): Promise<Map<string, InputType>> => {
await delay(1000);
const defaults = new Map<string, InputType>();
defaults.set("choiceInput", SessionStorageUtility.getEntry("choiceInput"));
defaults.set("booleanInput", SessionStorageUtility.getEntry("booleanInput") === "true");
defaults.set("stringInput", SessionStorageUtility.getEntry("stringInput"));
const numberSliderInput = parseInt(SessionStorageUtility.getEntry("numberSliderInput"));
defaults.set("numberSliderInput", !isNaN(numberSliderInput) ? numberSliderInput : 1);
const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("numberSpinnerInput"));
defaults.set("numberSpinnerInput", !isNaN(numberSpinnerInput) ? numberSpinnerInput : 1);
return defaults;
};
export const initializeNumberMaxValue = async (): Promise<number> => {
await delay(2000);
return 5;
};
export const descriptionElement = <Text>This is an example of Self serve class.</Text>;
export const renderText = (text: string): ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) => {
const elementPromiseFunction = async (currentValues: Map<string, InputType>): Promise<JSX.Element> => {
return <TextComponent text={text} currentValues={currentValues} />;
};
return elementPromiseFunction;
};

View File

@ -0,0 +1,175 @@
import { PropertyInfo, OnChange, Values } from "../PropertyDecorators";
import { ClassInfo, IsDisplayable } from "../ClassDecorators";
import { SelfServeBaseClass } from "../SelfServeUtils";
import { DropdownItem, Info, InputType, UiType } from "../../Explorer/Controls/SmartUi/SmartUiComponent";
import { SessionStorageUtility } from "../../Shared/StorageUtility";
export enum Regions {
NorthCentralUS = "NCUS",
WestUS = "WUS",
EastUS2 = "EUS2"
}
export const regionDropdownItems: DropdownItem[] = [
{ label: "North Central US", key: Regions.NorthCentralUS },
{ label: "West US", key: Regions.WestUS },
{ label: "East US 2", key: Regions.EastUS2 }
];
export const selfServeExampleInfo: Info = {
message: "This is a self serve class"
};
export const regionDropdownInfo: Info = {
message: "More regions can be added in the future."
};
export const delay = (ms: number): Promise<void> => {
console.log("delay called");
return new Promise(resolve => setTimeout(resolve, ms));
};
const onDbThroughputChange = (currentState: Map<string, InputType>, newValue: InputType): Map<string, InputType> => {
currentState.set("dbThroughput", newValue);
currentState.set("collectionThroughput", newValue);
return currentState;
};
const initializeMaxThroughput = async (): Promise<number> => {
await delay(2000);
return 10000;
};
/*
This is an example self serve class that auto generates UI components for your feature.
Each self serve class
- Needs to extends the SelfServeBase class.
- Needs to have the @IsDisplayable() decorator to tell the compiler that UI needs to be generated from this class.
- Needs to define an onSubmit() function, a callback for when the submit button is clicked.
- Needs to define an initialize() function, to set default values for the inputs.
You can test this self serve UI by using the featureflag '?feature.selfServeTypeForTest=example'
and plumb in similar feature flags for your own self serve class.
*/
/*
@IsDisplayable()
- role: Generated the JSON required to convert this class into the required UI. This is done during compile time.
*/
@IsDisplayable()
/*
@ClassInfo()
- optional
- input: Info | () => Promise<Info>
- role: Display an Info bar as the first element of the UI.
*/
@ClassInfo(selfServeExampleInfo)
export default class SelfServeExample extends SelfServeBaseClass {
/*
onSubmit()
- input: (currentValues: Map<string, InputType>) => Promise<void>
- role: Callback that is triggerred when the submit button is clicked. You should perform your rest API
calls here using the data from the different inputs passed as a Map to this callback function.
In this example, the onSubmit callback simply sets the value for keys corresponding to the field name
in the SessionStorage.
*/
public onSubmit = async (currentValues: Map<string, InputType>): Promise<void> => {
await delay(1000);
SessionStorageUtility.setEntry("regions", currentValues.get("regions")?.toString());
SessionStorageUtility.setEntry("enableLogging", currentValues.get("enableLogging")?.toString());
SessionStorageUtility.setEntry("accountName", currentValues.get("accountName")?.toString());
SessionStorageUtility.setEntry("dbThroughput", currentValues.get("dbThroughput")?.toString());
SessionStorageUtility.setEntry("collectionThroughput", currentValues.get("collectionThroughput")?.toString());
};
/*
initialize()
- input: () => Promise<Map<string, InputType>>
- role: Set default values for the properties of this class.
The properties of this class (namely regions, enableLogging, accountName, dbThroughput, collectionThroughput),
having the @Values decorator, will each correspond to an UI element. Their values can be of 'InputType'. Their
defaults can be set by setting values in a Map corresponding to the field's name.
Typically, you can make rest calls in the async initialize function, to fetch the initial values for
these fields. This is called after the onSubmit callback, to reinitialize the defaults.
In this example, the initialize function simply reads the SessionStorage to fetch the default values
for these fields. These are then set when the changes are submitted.
*/
public initialize = async (): Promise<Map<string, InputType>> => {
await delay(1000);
const defaults = new Map<string, InputType>();
defaults.set("regions", SessionStorageUtility.getEntry("regions"));
defaults.set("enableLogging", SessionStorageUtility.getEntry("enableLogging") === "true");
const stringInput = SessionStorageUtility.getEntry("accountName");
defaults.set("accountName", stringInput ? stringInput : "");
const numberSliderInput = parseInt(SessionStorageUtility.getEntry("dbThroughput"));
defaults.set("dbThroughput", isNaN(numberSliderInput) ? 1 : numberSliderInput);
const numberSpinnerInput = parseInt(SessionStorageUtility.getEntry("collectionThroughput"));
defaults.set("collectionThroughput", isNaN(numberSpinnerInput) ? 1 : numberSpinnerInput);
return defaults;
};
/*
@PropertyInfo()
- optional
- input: Info | () => Promise<Info>
- role: Display an Info bar above the UI element for this property.
*/
@PropertyInfo(regionDropdownInfo)
/*
@Values() :
- input: NumberInputOptions | StringInputOptions | BooleanInputOptions | DropdownInputOptions
- role: Specifies the required options to display the property as TextBox, Number Spinner/Slider, Radio buton or Dropdown.
*/
@Values({ label: "Regions", choices: regionDropdownItems })
regions: DropdownItem;
@Values({
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable"
})
enableLogging: boolean;
@Values({
label: "Account Name",
placeholder: "Enter the account name"
})
accountName: string;
/*
@OnChange()
- optional
- input: (currentValues: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
- role: Takes a Map of current values and the newValue for this property as inputs. This is called when a property
changes its value in the UI. This can be used to change other input values based on some other input.
The new Map of propertyName -> value is returned.
In this example, the onDbThroughputChange function sets the collectionThroughput to the same value as the dbThroughput
when the slider in moved in the UI.
*/
@OnChange(onDbThroughputChange)
@Values({
label: "Database Throughput",
min: 400,
max: initializeMaxThroughput,
step: 100,
uiType: UiType.Slider
})
dbThroughput: number;
@Values({
label: "Collection Throughput",
min: 400,
max: initializeMaxThroughput,
step: 100,
uiType: UiType.Spinner
})
collectionThroughput: number;
}

View File

@ -0,0 +1,107 @@
import { DropdownItem, Info, InputType, UiType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { addPropertyToMap } from "./SelfServeUtils";
interface Decorator {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
}
interface InputOptionsBase {
label: string;
}
export interface NumberInputOptions extends InputOptionsBase {
min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number;
uiType: UiType;
}
export interface StringInputOptions extends InputOptionsBase {
placeholder?: (() => Promise<string>) | string;
}
export interface BooleanInputOptions extends InputOptionsBase {
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
}
export interface DropdownInputOptions extends InputOptionsBase {
choices: (() => Promise<DropdownItem[]>) | DropdownItem[];
}
type InputOptions = NumberInputOptions | StringInputOptions | BooleanInputOptions | DropdownInputOptions;
function isNumberInputOptions(inputOptions: InputOptions): inputOptions is NumberInputOptions {
return !!(inputOptions as NumberInputOptions).min;
}
function isBooleanInputOptions(inputOptions: InputOptions): inputOptions is BooleanInputOptions {
return !!(inputOptions as BooleanInputOptions).trueLabel;
}
function isDropdownInputOptions(inputOptions: InputOptions): inputOptions is DropdownInputOptions {
return !!(inputOptions as DropdownInputOptions).choices;
}
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
return (target, property) => {
let className = target.constructor.name;
const propertyName = property.toString();
if (className === "Function") {
className = (target as Function).name;
throw new Error(`Property '${propertyName}' in class '${className}'should be not be static.`);
}
const propertyType = (Reflect.getMetadata("design:type", target, property)?.name as string)?.toLowerCase();
addPropertyToMap(target, propertyName, className, "type", propertyType);
addPropertyToMap(target, propertyName, className, "dataFieldName", propertyName);
decorators.map((decorator: Decorator) =>
addPropertyToMap(target, propertyName, className, decorator.name, decorator.value)
);
};
};
export const OnChange = (
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
): PropertyDecorator => {
return addToMap({ name: "onChange", value: onChange });
};
export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecorator => {
return addToMap({ name: "info", value: info });
};
export const Values = (inputOptions: InputOptions): PropertyDecorator => {
if (isNumberInputOptions(inputOptions)) {
const numberInputOptions = inputOptions as NumberInputOptions;
return addToMap(
{ name: "label", value: numberInputOptions.label },
{ name: "min", value: numberInputOptions.min },
{ name: "max", value: numberInputOptions.max },
{ name: "step", value: numberInputOptions.step },
{ name: "uiType", value: numberInputOptions.uiType }
);
} else if (isBooleanInputOptions(inputOptions)) {
const booleanInputOptions = inputOptions as BooleanInputOptions;
return addToMap(
{ name: "label", value: booleanInputOptions.label },
{ name: "trueLabel", value: booleanInputOptions.trueLabel },
{ name: "falseLabel", value: booleanInputOptions.falseLabel }
);
} else if (isDropdownInputOptions(inputOptions)) {
const dropdownInputOptions = inputOptions as DropdownInputOptions;
return addToMap(
{ name: "label", value: dropdownInputOptions.label },
{ name: "choices", value: dropdownInputOptions.choices }
);
} else {
const stringInputOptions = inputOptions as StringInputOptions;
return addToMap(
{ name: "label", value: stringInputOptions.label },
{ name: "placeholder", value: stringInputOptions.placeholder }
);
}
};

View File

@ -1,84 +0,0 @@
import { ChoiceItem, Info, InputType, NumberInputType } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { addPropertyToMap } from "./SelfServeUtils";
interface Decorator {
name: string;
value: unknown;
}
const addToMap = (...decorators: Decorator[]): PropertyDecorator => {
return (target, property) => {
const className = (target as Function).name;
const propertyType = (Reflect.getMetadata("design:type", target, property).name as string).toLowerCase();
addPropertyToMap(target, property.toString(), className, "type", propertyType);
addPropertyToMap(target, property.toString(), className, "dataFieldName", property.toString());
if (!className) {
throw new Error("property descriptor applied to non static field!");
}
decorators.map((decorator: Decorator) =>
addPropertyToMap(target, property.toString(), className, decorator.name, decorator.value)
);
};
};
export const OnChange = (
onChange: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>
): PropertyDecorator => {
return addToMap({ name: "onChange", value: onChange });
};
export const CustomElement = (
customElement: ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) | JSX.Element
): PropertyDecorator => {
return addToMap({ name: "customElement", value: customElement });
};
export const PropertyInfo = (info: (() => Promise<Info>) | Info): PropertyDecorator => {
return addToMap({ name: "info", value: info });
};
export const Placeholder = (placeholder: (() => Promise<string>) | string): PropertyDecorator => {
return addToMap({ name: "placeholder", value: placeholder });
};
export const ParentOf = (children: string[]): PropertyDecorator => {
return addToMap({ name: "parentOf", value: children });
};
export const Label = (label: (() => Promise<string>) | string): PropertyDecorator => {
return addToMap({ name: "label", value: label });
};
export interface NumberInputOptions {
min: (() => Promise<number>) | number;
max: (() => Promise<number>) | number;
step: (() => Promise<number>) | number;
numberInputType: NumberInputType;
}
export const NumberInput = (numberInputOptions: NumberInputOptions): PropertyDecorator => {
return addToMap(
{ name: "min", value: numberInputOptions.min },
{ name: "max", value: numberInputOptions.max },
{ name: "step", value: numberInputOptions.step },
{ name: "inputType", value: numberInputOptions.numberInputType }
);
};
export interface BooleanInputOptions {
trueLabel: (() => Promise<string>) | string;
falseLabel: (() => Promise<string>) | string;
}
export const BooleanInput = (booleanInputOptions: BooleanInputOptions): PropertyDecorator => {
return addToMap(
{ name: "trueLabel", value: booleanInputOptions.trueLabel },
{ name: "falseLabel", value: booleanInputOptions.falseLabel }
);
};
export const ChoiceInput = (choices: (() => Promise<ChoiceItem[]>) | ChoiceItem[]): PropertyDecorator => {
return addToMap({ name: "choices", value: choices });
};

View File

@ -9,7 +9,6 @@ import { ReactAdapter } from "../Bindings/ReactBindingHandler";
import Explorer from "../Explorer/Explorer";
import { Descriptor, SmartUiComponent } from "../Explorer/Controls/SmartUi/SmartUiComponent";
import { SelfServeTypes } from "./SelfServeUtils";
import { SelfServeExample } from "./Example/Example";
export class SelfServeComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
@ -23,18 +22,20 @@ export class SelfServeComponentAdapter implements ReactAdapter {
});
}
private getDescriptor = (selfServeType: SelfServeTypes): Descriptor => {
public static getDescriptor = async (selfServeType: SelfServeTypes): Promise<Descriptor> => {
switch (selfServeType) {
case SelfServeTypes.example:
return SelfServeExample.toSmartUiDescriptor();
case SelfServeTypes.example: {
const SelfServeExample = await import(/* webpackChunkName: "SelfServeExample" */ "./Example/SelfServeExample");
return new SelfServeExample.default().toSmartUiDescriptor();
}
default:
return undefined;
}
};
public renderComponent(): JSX.Element {
public async renderComponent(): Promise<JSX.Element> {
const selfServeType = this.container.selfServeType();
const smartUiDescriptor = this.getDescriptor(selfServeType);
const smartUiDescriptor = await SelfServeComponentAdapter.getDescriptor(selfServeType);
const element = smartUiDescriptor ? (
<SmartUiComponent descriptor={smartUiDescriptor} />

View File

@ -0,0 +1,275 @@
import {
CommonInputTypes,
mapToSmartUiDescriptor,
SelfServeBaseClass,
updateContextWithDecorator
} from "./SelfServeUtils";
import { InputType, UiType } from "./../Explorer/Controls/SmartUi/SmartUiComponent";
describe("SelfServeUtils", () => {
it("initialize should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass {
public onSubmit = async (): Promise<void> => {
return;
};
public initialize: () => Promise<Map<string, InputType>>;
}
expect(() => new Test().toSmartUiDescriptor()).toThrow("initialize() was not declared for the class 'Test'");
});
it("onSubmit should be declared for self serve classes", () => {
class Test extends SelfServeBaseClass {
public onSubmit: () => Promise<void>;
public initialize = async (): Promise<Map<string, InputType>> => {
return undefined;
};
}
expect(() => new Test().toSmartUiDescriptor()).toThrow("onSubmit() was not declared for the class 'Test'");
});
it("@SmartUi decorator must be present for self serve classes", () => {
class Test extends SelfServeBaseClass {
public onSubmit = async (): Promise<void> => {
return;
};
public initialize = async (): Promise<Map<string, InputType>> => {
return undefined;
};
}
expect(() => new Test().toSmartUiDescriptor()).toThrow("@SmartUi decorator was not declared for the class 'Test'");
});
it("updateContextWithDecorator", () => {
const context = new Map<string, CommonInputTypes>();
updateContextWithDecorator(context, "dbThroughput", "testClass", "max", 1);
updateContextWithDecorator(context, "dbThroughput", "testClass", "min", 2);
updateContextWithDecorator(context, "collThroughput", "testClass", "max", 5);
expect(context.size).toEqual(2);
expect(context.get("dbThroughput")).toEqual({ id: "dbThroughput", max: 1, min: 2 });
expect(context.get("collThroughput")).toEqual({ id: "collThroughput", max: 5 });
});
it("mapToSmartUiDescriptor", () => {
const context: Map<string, CommonInputTypes> = new Map([
[
"dbThroughput",
{
id: "dbThroughput",
dataFieldName: "dbThroughput",
type: "number",
label: "Database Throughput",
min: 1,
max: 5,
step: 1,
uiType: UiType.Slider
}
],
[
"collThroughput",
{
id: "collThroughput",
dataFieldName: "collThroughput",
type: "number",
label: "Coll Throughput",
min: 1,
max: 5,
step: 1,
uiType: UiType.Spinner
}
],
[
"invalidThroughput",
{
id: "invalidThroughput",
dataFieldName: "invalidThroughput",
type: "boolean",
label: "Invalid Coll Throughput",
min: 1,
max: 5,
step: 1,
uiType: UiType.Spinner,
errorMessage: "label, truelabel and falselabel are required for boolean input"
}
],
[
"collName",
{
id: "collName",
dataFieldName: "collName",
type: "string",
label: "Coll Name",
placeholder: "placeholder text"
}
],
[
"enableLogging",
{
id: "enableLogging",
dataFieldName: "enableLogging",
type: "boolean",
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable"
}
],
[
"invalidEnableLogging",
{
id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging",
type: "boolean",
label: "Invalid Enable Logging",
placeholder: "placeholder text"
}
],
[
"regions",
{
id: "regions",
dataFieldName: "regions",
type: "object",
label: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" }
]
}
],
[
"invalidRegions",
{
id: "invalidRegions",
dataFieldName: "invalidRegions",
type: "object",
label: "Invalid Regions",
placeholder: "placeholder text"
}
]
]);
const expectedDescriptor = {
root: {
id: "root",
children: [
{
id: "dbThroughput",
input: {
id: "dbThroughput",
dataFieldName: "dbThroughput",
type: "number",
label: "Database Throughput",
min: 1,
max: 5,
step: 1,
uiType: "Slider"
},
children: [] as Node[]
},
{
id: "collThroughput",
input: {
id: "collThroughput",
dataFieldName: "collThroughput",
type: "number",
label: "Coll Throughput",
min: 1,
max: 5,
step: 1,
uiType: "Spinner"
},
children: [] as Node[]
},
{
id: "invalidThroughput",
input: {
id: "invalidThroughput",
dataFieldName: "invalidThroughput",
type: "boolean",
label: "Invalid Coll Throughput",
min: 1,
max: 5,
step: 1,
uiType: "Spinner",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidThroughput'."
},
children: [] as Node[]
},
{
id: "collName",
input: {
id: "collName",
dataFieldName: "collName",
type: "string",
label: "Coll Name",
placeholder: "placeholder text"
},
children: [] as Node[]
},
{
id: "enableLogging",
input: {
id: "enableLogging",
dataFieldName: "enableLogging",
type: "boolean",
label: "Enable Logging",
trueLabel: "Enable",
falseLabel: "Disable"
},
children: [] as Node[]
},
{
id: "invalidEnableLogging",
input: {
id: "invalidEnableLogging",
dataFieldName: "invalidEnableLogging",
type: "boolean",
label: "Invalid Enable Logging",
placeholder: "placeholder text",
errorMessage: "label, truelabel and falselabel are required for boolean input 'invalidEnableLogging'."
},
children: [] as Node[]
},
{
id: "regions",
input: {
id: "regions",
dataFieldName: "regions",
type: "object",
label: "Regions",
choices: [
{ label: "South West US", key: "SWUS" },
{ label: "North Central US", key: "NCUS" },
{ label: "East US 2", key: "EUS2" }
]
},
children: [] as Node[]
},
{
id: "invalidRegions",
input: {
id: "invalidRegions",
dataFieldName: "invalidRegions",
type: "object",
label: "Invalid Regions",
placeholder: "placeholder text",
errorMessage: "label and choices are required for Dropdown input 'invalidRegions'."
},
children: [] as Node[]
}
]
},
inputNames: [
"dbThroughput",
"collThroughput",
"invalidThroughput",
"collName",
"enableLogging",
"invalidEnableLogging",
"regions",
"invalidRegions"
]
};
const descriptor = mapToSmartUiDescriptor(context);
expect(descriptor).toEqual(expectedDescriptor);
});
});

View File

@ -1,6 +1,6 @@
import "reflect-metadata";
import {
ChoiceItem,
DropdownItem,
Node,
Info,
InputTypeValue,
@ -9,22 +9,43 @@ import {
NumberInput,
StringInput,
BooleanInput,
ChoiceInput,
DropdownInput,
InputType
} from "../Explorer/Controls/SmartUi/SmartUiComponent";
const SelfServeType = "selfServeType";
export enum SelfServeTypes {
none = "none",
invalid = "invalid",
example = "example"
}
export class SelfServeBase {
public static toSmartUiDescriptor(): Descriptor {
return Reflect.getMetadata(this.name, this) as Descriptor;
export abstract class SelfServeBaseClass {
public abstract onSubmit: (currentValues: Map<string, InputType>) => Promise<void>;
public abstract initialize: () => Promise<Map<string, InputType>>;
public toSmartUiDescriptor(): Descriptor {
const className = this.constructor.name;
const smartUiDescriptor = Reflect.getMetadata(className, this) as Descriptor;
if (!this.initialize) {
throw new Error(`initialize() was not declared for the class '${className}'`);
}
if (!this.onSubmit) {
throw new Error(`onSubmit() was not declared for the class '${className}'`);
}
if (!smartUiDescriptor?.root) {
throw new Error(`@SmartUi decorator was not declared for the class '${className}'`);
}
smartUiDescriptor.initialize = this.initialize;
smartUiDescriptor.onSubmit = this.onSubmit;
return smartUiDescriptor;
}
}
export interface CommonInputTypes {
id: string;
info?: (() => Promise<Info>) | Info;
parentOf?: string[];
type?: InputTypeValue;
label?: (() => Promise<string>) | string;
placeholder?: (() => Promise<string>) | string;
@ -34,12 +55,12 @@ export interface CommonInputTypes {
step?: (() => Promise<number>) | number;
trueLabel?: (() => Promise<string>) | string;
falseLabel?: (() => Promise<string>) | string;
choices?: (() => Promise<ChoiceItem[]>) | ChoiceItem[];
inputType?: string;
choices?: (() => Promise<DropdownItem[]>) | DropdownItem[];
uiType?: string;
errorMessage?: string;
onChange?: (currentState: Map<string, InputType>, newValue: InputType) => Map<string, InputType>;
onSubmit?: (currentValues: Map<string, InputType>) => Promise<void>;
initialize?: () => Promise<Map<string, InputType>>;
customElement?: ((currentValues: Map<string, InputType>) => Promise<JSX.Element>) | JSX.Element;
}
const setValue = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
@ -54,95 +75,86 @@ const getValue = <T extends keyof CommonInputTypes>(name: T, fieldObject: Common
return fieldObject[name];
};
export const addPropertyToMap = (
target: Object,
propertyKey: string,
metadataKey: string,
export const addPropertyToMap = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
target: unknown,
propertyName: string,
className: string,
descriptorName: string,
descriptorValue: any
descriptorValue: K
): void => {
const descriptorKey = descriptorName.toString() as keyof CommonInputTypes;
let context = Reflect.getMetadata(metadataKey, target) as Map<string, CommonInputTypes>;
let context = Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>;
if (!context) {
context = new Map<string, CommonInputTypes>();
}
updateContextWithDecorator(context, propertyName, className, descriptorName, descriptorValue);
Reflect.defineMetadata(className, context, target);
};
export const updateContextWithDecorator = <T extends keyof CommonInputTypes, K extends CommonInputTypes[T]>(
context: Map<string, CommonInputTypes>,
propertyName: string,
className: string,
descriptorName: string,
descriptorValue: K
): void => {
const descriptorKey = descriptorName as keyof CommonInputTypes;
if (!(context instanceof Map)) {
throw new Error("@SmartUi should be the first decorator for the class.");
console.log(context);
throw new Error(`@SmartUi should be the first decorator for the class '${className}'.`);
}
let propertyObject = context.get(propertyKey);
let propertyObject = context.get(propertyName);
if (!propertyObject) {
propertyObject = { id: propertyKey };
propertyObject = { id: propertyName };
}
if (getValue(descriptorKey, propertyObject) && descriptorKey !== "type" && descriptorKey !== "dataFieldName") {
throw new Error("duplicate descriptor");
throw new Error(
`Duplicate value passed for '${descriptorKey}' on property '${propertyName}' of class '${className}'`
);
}
setValue(descriptorKey, descriptorValue, propertyObject);
context.set(propertyKey, propertyObject);
Reflect.defineMetadata(metadataKey, context, target);
context.set(propertyName, propertyObject);
};
export const toSmartUiDescriptor = (metadataKey: string, target: Object): void => {
const context = Reflect.getMetadata(metadataKey, target) as Map<string, CommonInputTypes>;
Reflect.defineMetadata(metadataKey, context, target);
export const buildSmartUiDescriptor = (className: string, target: unknown): void => {
const context = Reflect.getMetadata(className, target) as Map<string, CommonInputTypes>;
const smartUiDescriptor = mapToSmartUiDescriptor(context);
Reflect.defineMetadata(className, smartUiDescriptor, target);
};
export const mapToSmartUiDescriptor = (context: Map<string, CommonInputTypes>): Descriptor => {
const root = context.get("root");
context.delete("root");
const inputNames: string[] = [];
if (!root?.onSubmit) {
throw new Error(
"@OnSubmit decorator not declared for the class. Please ensure @SmartUi is the first decorator used for the class."
);
}
if (!root?.initialize) {
throw new Error(
"@Initialize decorator not declared for the class. Please ensure @SmartUi is the first decorator used for the class."
);
}
const smartUiDescriptor = {
onSubmit: root.onSubmit,
initialize: root.initialize,
const smartUiDescriptor: Descriptor = {
root: {
id: "root",
info: root.info,
info: root?.info,
children: []
} as Node
} as Descriptor;
}
};
while (context.size > 0) {
const key = context.keys().next().value;
addToDescriptor(context, smartUiDescriptor, smartUiDescriptor.root, key);
addToDescriptor(context, smartUiDescriptor.root, key, inputNames);
}
smartUiDescriptor.inputNames = inputNames;
Reflect.defineMetadata(metadataKey, smartUiDescriptor, target);
return smartUiDescriptor;
};
const addToDescriptor = (
context: Map<string, CommonInputTypes>,
smartUiDescriptor: Descriptor,
root: Node,
key: string
key: string,
inputNames: string[]
): void => {
const value = context.get(key);
if (!value) {
// should already be added to root
const childNode = getChildFromRoot(key, smartUiDescriptor);
if (!childNode) {
// if not found at root level, error out
throw new Error("Either child does not exist or child has been assigned to more than one parent");
}
root.children.push(childNode);
return;
}
const childrenKeys = value.parentOf;
inputNames.push(value.id);
const element = {
id: value.id,
info: value.info,
@ -150,61 +162,30 @@ const addToDescriptor = (
children: []
} as Node;
context.delete(key);
for (const childKey in childrenKeys) {
addToDescriptor(context, smartUiDescriptor, element, childrenKeys[childKey]);
}
root.children.push(element);
};
const getChildFromRoot = (key: string, smartUiDescriptor: Descriptor): Node => {
let i = 0;
const children = smartUiDescriptor.root.children;
while (i < children.length) {
if (children[i]?.id === key) {
const value = children[i];
delete children[i];
return value;
} else {
i++;
}
}
return undefined;
};
const getInput = (value: CommonInputTypes): AnyInput => {
if (!value.label && !value.customElement) {
throw new Error("label is required.");
}
switch (value.type) {
case "number":
if (!value.step || !value.inputType || !value.min || !value.max) {
throw new Error("step, min, miax and inputType are needed for number type");
if (!value.label || !value.step || !value.uiType || !value.min || !value.max) {
value.errorMessage = `label, step, min, max and uiType are required for number input '${value.id}'.`;
}
return value as NumberInput;
case "string":
if (!value.label) {
value.errorMessage = `label is required for string input '${value.id}'.`;
}
return value as StringInput;
case "boolean":
if (!value.trueLabel || !value.falseLabel) {
throw new Error("truelabel and falselabel are needed for boolean type");
if (!value.label || !value.trueLabel || !value.falseLabel) {
value.errorMessage = `label, truelabel and falselabel are required for boolean input '${value.id}'.`;
}
return value as BooleanInput;
default:
if (!value.choices) {
throw new Error("choices are needed for enum type");
if (!value.label || !value.choices) {
value.errorMessage = `label and choices are required for Dropdown input '${value.id}'.`;
}
return value as ChoiceInput;
return value as DropdownInput;
}
};
export enum SelfServeTypes {
none = "none",
invalid = "invalid",
example = "example"
}
export const getSelfServeType = (search: string): SelfServeTypes => {
const params = new URLSearchParams(search);
const selfServeTypeParam = params.get(SelfServeType)?.toLowerCase();
return SelfServeTypes[selfServeTypeParam as keyof typeof SelfServeTypes];
};

View File

@ -1,59 +1,9 @@
import { ElementHandle, Frame } from "puppeteer";
import { TestExplorerParams } from "./testExplorer/TestExplorerParams";
import * as path from "path";
export const NOTEBOOK_OPERATION_DELAY = 5000;
export const RENDER_DELAY = 2500;
let testExplorerFrame: Frame;
export const getTestExplorerFrame = async (): Promise<Frame> => {
if (testExplorerFrame) {
return testExplorerFrame;
}
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerTenantId,
encodeURI(notebooksTestRunnerTenantId)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerClientId,
encodeURI(notebooksTestRunnerClientId)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerClientSecret,
encodeURI(notebooksTestRunnerClientSecret)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccount,
encodeURI(portalRunnerDatabaseAccount)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccountKey,
encodeURI(portalRunnerDatabaseAccountKey)
);
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerResourceGroup,
encodeURI(portalRunnerResourceGroup)
);
await page.goto(testExplorerUrl.toString());
const handle = await page.waitForSelector("iframe");
testExplorerFrame = await handle.contentFrame();
await testExplorerFrame.waitForSelector(".galleryHeader");
return testExplorerFrame;
};
export const uploadNotebookIfNotExist = async (frame: Frame, notebookName: string): Promise<ElementHandle<Element>> => {
const notebookNode = await getNotebookNode(frame, notebookName);
if (notebookNode) {

View File

@ -1,6 +1,6 @@
import "expect-puppeteer";
import { getTestExplorerFrame, uploadNotebookIfNotExist } from "./notebookTestUtils";
import { uploadNotebookIfNotExist } from "./notebookTestUtils";
import { ElementHandle, Frame } from "puppeteer";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
jest.setTimeout(300000);
@ -12,6 +12,7 @@ describe("Notebook UI tests", () => {
it("Upload, Open and Delete Notebook", async () => {
try {
frame = await getTestExplorerFrame();
await frame.waitForSelector(".galleryHeader");
uploadedNotebookNode = await uploadNotebookIfNotExist(frame, notebookName);
await uploadedNotebookNode.click();
await frame.waitForSelector(".tabNavText");

View File

@ -0,0 +1,29 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "../testExplorer/TestExplorerParams";
import { getTestExplorerFrame } from "../testExplorer/TestExplorerUtils";
import { SelfServeTypes } from "../../src/SelfServe/SelfServeUtils";
jest.setTimeout(300000);
let frame: Frame;
describe("Notebook UI tests", () => {
it("Upload, Open and Delete Notebook", async () => {
try {
frame = await getTestExplorerFrame(
new Map<string, string>([[TestExplorerParams.selfServeType, SelfServeTypes.example]])
);
await frame.waitForSelector(".ms-Dropdown");
const dropdownLabel = await frame.$eval(".ms-Dropdown-label", element => element.textContent);
expect(dropdownLabel).toEqual("Regions");
await frame.waitForSelector(".radioSwitchComponent");
await frame.waitForSelector(".ms-TextField");
await frame.waitForSelector(".ms-Slider ");
await frame.waitForSelector(".ms-spinButton-input");
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const testName = (expect as any).getState().currentTestName;
await page.screenshot({ path: `Test Failed ${testName}.jpg` });
throw error;
}
});
});

View File

@ -1,11 +1,11 @@
import { MessageTypes } from "../../../src/Contracts/ExplorerContracts";
import "../../../less/hostedexplorer.less";
import { MessageTypes } from "../../src/Contracts/ExplorerContracts";
import "../../less/hostedexplorer.less";
import { TestExplorerParams } from "./TestExplorerParams";
import { ClientSecretCredential } from "@azure/identity";
import { DatabaseAccountsGetResponse } from "@azure/arm-cosmosdb/esm/models";
import { CosmosDBManagementClient } from "@azure/arm-cosmosdb";
import * as msRest from "@azure/ms-rest-js";
import * as ViewModels from "../../../src/Contracts/ViewModels";
import * as ViewModels from "../../src/Contracts/ViewModels";
class CustomSigner implements msRest.ServiceClientCredentials {
private token: string;
@ -87,6 +87,7 @@ const initTestExplorer = async (): Promise<void> => {
const portalRunnerResourceGroup = decodeURIComponent(
urlSearchParams.get(TestExplorerParams.portalRunnerResourceGroup)
);
const selfServeType = urlSearchParams.get(TestExplorerParams.selfServeType);
const token = await AADLogin(
notebooksTestRunnerTenantId,
@ -128,7 +129,8 @@ const initTestExplorer = async (): Promise<void> => {
throughput: { fixed: 400, unlimited: 400, unlimitedmax: 100000, unlimitedmin: 400, shared: 400 }
},
// add UI test only when feature is not dependent on flights anymore
flights: []
flights: [],
selfServeType: selfServeType
} as ViewModels.DataExplorerInputsFrame
};

View File

@ -5,5 +5,6 @@ export enum TestExplorerParams {
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
portalRunnerSubscripton = "portalRunnerSubscripton",
portalRunnerResourceGroup = "portalRunnerResourceGroup"
portalRunnerResourceGroup = "portalRunnerResourceGroup",
selfServeType = "selfServeType"
}

View File

@ -0,0 +1,54 @@
import { Frame } from "puppeteer";
import { TestExplorerParams } from "./TestExplorerParams";
let testExplorerFrame: Frame;
export const getTestExplorerFrame = async (params?: Map<string, string>): Promise<Frame> => {
if (testExplorerFrame) {
return testExplorerFrame;
}
const notebooksTestRunnerTenantId = process.env.NOTEBOOKS_TEST_RUNNER_TENANT_ID;
const notebooksTestRunnerClientId = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_ID;
const notebooksTestRunnerClientSecret = process.env.NOTEBOOKS_TEST_RUNNER_CLIENT_SECRET;
const portalRunnerDatabaseAccount = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT;
const portalRunnerDatabaseAccountKey = process.env.PORTAL_RUNNER_DATABASE_ACCOUNT_KEY;
const portalRunnerSubscripton = process.env.PORTAL_RUNNER_SUBSCRIPTION;
const portalRunnerResourceGroup = process.env.PORTAL_RUNNER_RESOURCE_GROUP;
const testExplorerUrl = new URL("testExplorer.html", "https://localhost:1234");
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerTenantId,
encodeURI(notebooksTestRunnerTenantId)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerClientId,
encodeURI(notebooksTestRunnerClientId)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.notebooksTestRunnerClientSecret,
encodeURI(notebooksTestRunnerClientSecret)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccount,
encodeURI(portalRunnerDatabaseAccount)
);
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerDatabaseAccountKey,
encodeURI(portalRunnerDatabaseAccountKey)
);
testExplorerUrl.searchParams.append(TestExplorerParams.portalRunnerSubscripton, encodeURI(portalRunnerSubscripton));
testExplorerUrl.searchParams.append(
TestExplorerParams.portalRunnerResourceGroup,
encodeURI(portalRunnerResourceGroup)
);
if (params) {
for (const key of params.keys()) {
testExplorerUrl.searchParams.append(key, encodeURI(params.get(key)));
}
}
await page.goto(testExplorerUrl.toString());
const handle = await page.waitForSelector("iframe");
return await handle.contentFrame();
};

View File

@ -21,6 +21,6 @@
"noEmit": true,
"types": ["jest"]
},
"include": ["./src/**/*", "./test/notebooks/testExplorer/**/*"],
"include": ["./src/**/*", "test/testExplorer/**/*"],
"exclude": ["./src/**/__mocks__/**/*"]
}

View File

@ -142,7 +142,7 @@ module.exports = function(env = {}, argv = {}) {
}),
new HtmlWebpackPlugin({
filename: "testExplorer.html",
template: "test/notebooks/testExplorer/testExplorer.html",
template: "test/testExplorer/testExplorer.html",
chunks: ["testExplorer"]
}),
new HtmlWebpackPlugin({
@ -183,7 +183,7 @@ module.exports = function(env = {}, argv = {}) {
index: "./src/Index.ts",
quickstart: "./src/quickstart.ts",
hostedExplorer: "./src/HostedExplorer.ts",
testExplorer: "./test/notebooks/testExplorer/TestExplorer.ts",
testExplorer: "./test/testExplorer/TestExplorer.ts",
heatmap: "./src/Controls/Heatmap/Heatmap.ts",
terminal: "./src/Terminal/index.ts",
notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",