mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-10-13 15:28:05 +01:00
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:
parent
373327dc88
commit
318842624f
@ -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
28
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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],
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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)}
|
||||
|
@ -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%",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
@ -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 =
|
||||
|
46
src/Main.tsx
46
src/Main.tsx
@ -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 */}
|
||||
|
14
src/SelfServe/ClassDecorators.tsx
Normal file
14
src/SelfServe/ClassDecorators.tsx
Normal 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);
|
||||
};
|
||||
};
|
@ -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);
|
||||
};
|
||||
};
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
};
|
175
src/SelfServe/Example/SelfServeExample.tsx
Normal file
175
src/SelfServe/Example/SelfServeExample.tsx
Normal 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;
|
||||
}
|
107
src/SelfServe/PropertyDecorators.tsx
Normal file
107
src/SelfServe/PropertyDecorators.tsx
Normal 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 }
|
||||
);
|
||||
}
|
||||
};
|
@ -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 });
|
||||
};
|
@ -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} />
|
||||
|
275
src/SelfServe/SelfServeUtils.test.tsx
Normal file
275
src/SelfServe/SelfServeUtils.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
@ -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];
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
29
test/selfServe/selfServeExample.spec.ts
Normal file
29
test/selfServe/selfServeExample.spec.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
||||
|
@ -5,5 +5,6 @@ export enum TestExplorerParams {
|
||||
portalRunnerDatabaseAccount = "portalRunnerDatabaseAccount",
|
||||
portalRunnerDatabaseAccountKey = "portalRunnerDatabaseAccountKey",
|
||||
portalRunnerSubscripton = "portalRunnerSubscripton",
|
||||
portalRunnerResourceGroup = "portalRunnerResourceGroup"
|
||||
portalRunnerResourceGroup = "portalRunnerResourceGroup",
|
||||
selfServeType = "selfServeType"
|
||||
}
|
54
test/testExplorer/TestExplorerUtils.ts
Normal file
54
test/testExplorer/TestExplorerUtils.ts
Normal 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();
|
||||
};
|
@ -21,6 +21,6 @@
|
||||
"noEmit": true,
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["./src/**/*", "./test/notebooks/testExplorer/**/*"],
|
||||
"include": ["./src/**/*", "test/testExplorer/**/*"],
|
||||
"exclude": ["./src/**/__mocks__/**/*"]
|
||||
}
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user