mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-24 19:31:36 +00:00
Compare commits
8 Commits
all-checks
...
tsStrict/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17fa6aff31 | ||
|
|
c42a10faa5 | ||
|
|
0d79f01304 | ||
|
|
eae5b2219e | ||
|
|
2fda881770 | ||
|
|
35f8fa8324 | ||
|
|
0e413430dc | ||
|
|
afd7f43eb8 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -214,10 +214,3 @@ jobs:
|
||||
name: packages
|
||||
with:
|
||||
path: "*.nupkg"
|
||||
allcheckspass:
|
||||
name: "All Checks Pass"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: "WyriHaximus/github-action-wait-for-status@v1.3.1"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -32,7 +32,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
<DatePicker
|
||||
className="addEntityDatePicker"
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={entityValue && new Date(entityValue)}
|
||||
value={entityValue ? new Date(entityValue) : new Date()}
|
||||
ariaLabel={entityValuePlaceholder}
|
||||
onSelectDate={onSelectDate}
|
||||
disabled={isEntityValueDisable}
|
||||
@@ -59,7 +59,7 @@ export const EntityValue: FunctionComponent<TableEntityProps> = ({
|
||||
disabled={isEntityValueDisable}
|
||||
type={entityValueType}
|
||||
placeholder={entityValuePlaceholder}
|
||||
value={typeof entityValue === "string" && entityValue}
|
||||
value={typeof entityValue === "string" ? entityValue : ""}
|
||||
onChange={onEntityValueChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { DefaultDirectoryDropdownComponent, DefaultDirectoryDropdownProps } from "./DefaultDirectoryDropdownComponent";
|
||||
import { Tenant } from "../../../Contracts/DataModels";
|
||||
|
||||
const createBlankProps = (): DefaultDirectoryDropdownProps => {
|
||||
return {
|
||||
defaultDirectoryId: "",
|
||||
directories: [],
|
||||
onDefaultDirectoryChange: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createBlankDirectory = (): Tenant => {
|
||||
return {
|
||||
countryCode: "",
|
||||
displayName: "",
|
||||
domains: [],
|
||||
id: "",
|
||||
tenantId: "",
|
||||
};
|
||||
};
|
||||
|
||||
describe("test render", () => {
|
||||
it("renders with no directories", () => {
|
||||
const props = createBlankProps();
|
||||
|
||||
const wrapper = shallow(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with directories but no default", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
|
||||
const wrapper = shallow(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with directories and default", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
|
||||
props.defaultDirectoryId = "asdfghjklzxcvbnm9876543210";
|
||||
|
||||
const wrapper = shallow(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with directories and last visit default", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
|
||||
props.defaultDirectoryId = "lastVisited";
|
||||
|
||||
const wrapper = shallow(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("test function", () => {
|
||||
it("on default directory change", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
props.defaultDirectoryId = "lastVisited";
|
||||
|
||||
const wrapper = mount(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
|
||||
wrapper.find("div.defaultDirectoryDropdown").find("div.ms-Dropdown").simulate("click");
|
||||
expect(wrapper.exists("div.ms-Callout-main")).toBe(true);
|
||||
wrapper.find("button.ms-Dropdown-item").at(1).simulate("click");
|
||||
expect(props.onDefaultDirectoryChange).toBeCalled();
|
||||
expect(props.onDefaultDirectoryChange).toHaveBeenCalled();
|
||||
|
||||
wrapper.find("div.defaultDirectoryDropdown").find("div.ms-Dropdown").simulate("click");
|
||||
expect(wrapper.exists("div.ms-Callout-main")).toBe(true);
|
||||
wrapper.find("button.ms-Dropdown-item").at(0).simulate("click");
|
||||
expect(props.onDefaultDirectoryChange).toBeCalled();
|
||||
expect(props.onDefaultDirectoryChange).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* React component for Switch Directory
|
||||
*/
|
||||
|
||||
import { Dropdown, IDropdownOption, IDropdownProps } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
import { Tenant } from "../../../Contracts/DataModels";
|
||||
|
||||
export interface DefaultDirectoryDropdownProps {
|
||||
directories: Array<Tenant>;
|
||||
defaultDirectoryId: string;
|
||||
onDefaultDirectoryChange: (newDirectory: Tenant) => void;
|
||||
}
|
||||
|
||||
export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDirectoryDropdownProps> {
|
||||
public static readonly lastVisitedKey: string = "lastVisited";
|
||||
|
||||
public render(): JSX.Element {
|
||||
const lastVisitedOption: IDropdownOption = {
|
||||
key: DefaultDirectoryDropdownComponent.lastVisitedKey,
|
||||
text: "Sign in to your last visited directory",
|
||||
};
|
||||
const directoryOptions: Array<IDropdownOption> = this.props.directories.map(
|
||||
(dirc): IDropdownOption => {
|
||||
return {
|
||||
key: dirc.tenantId,
|
||||
text: `${dirc.displayName}(${dirc.tenantId})`,
|
||||
};
|
||||
}
|
||||
);
|
||||
const dropDownOptions: Array<IDropdownOption> = [lastVisitedOption, ...directoryOptions];
|
||||
const dropDownProps: IDropdownProps = {
|
||||
label: "Set your default directory",
|
||||
options: dropDownOptions,
|
||||
defaultSelectedKey: this.props.defaultDirectoryId ? this.props.defaultDirectoryId : lastVisitedOption.key,
|
||||
onChange: this._onDropdownChange,
|
||||
className: "defaultDirectoryDropdown",
|
||||
};
|
||||
|
||||
return <Dropdown {...dropDownProps} />;
|
||||
}
|
||||
|
||||
private _onDropdownChange = (e: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void => {
|
||||
if (!option || !option.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.key === this.props.defaultDirectoryId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.key === DefaultDirectoryDropdownComponent.lastVisitedKey) {
|
||||
this.props.onDefaultDirectoryChange({
|
||||
tenantId: option.key,
|
||||
countryCode: undefined,
|
||||
displayName: undefined,
|
||||
domains: [],
|
||||
id: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === option.key);
|
||||
if (!selectedDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onDefaultDirectoryChange(selectedDirectory);
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { DirectoryListComponent, DirectoryListProps } from "./DirectoryListComponent";
|
||||
import { DefaultDirectoryDropdownComponent, DefaultDirectoryDropdownProps } from "./DefaultDirectoryDropdownComponent";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
|
||||
export class DirectoryComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(
|
||||
private _dropdownProps: ko.Observable<DefaultDirectoryDropdownProps>,
|
||||
private _listProps: ko.Observable<DirectoryListProps>
|
||||
) {
|
||||
this._dropdownProps.subscribe(() => this.forceRender());
|
||||
this._listProps.subscribe(() => this.forceRender());
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<div className="directoryDropdownContainer">
|
||||
<DefaultDirectoryDropdownComponent {...this._dropdownProps()} />
|
||||
</div>
|
||||
<div className="directoryDivider" />
|
||||
<div className="directoryListContainer">
|
||||
<DirectoryListComponent {...this._listProps()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public forceRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
import { DirectoryListComponent, DirectoryListProps } from "./DirectoryListComponent";
|
||||
import { Tenant } from "../../../Contracts/DataModels";
|
||||
|
||||
const createBlankProps = (): DirectoryListProps => {
|
||||
return {
|
||||
selectedDirectoryId: undefined,
|
||||
directories: [],
|
||||
onNewDirectorySelected: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
const createBlankDirectory = (): Tenant => {
|
||||
return {
|
||||
countryCode: undefined,
|
||||
displayName: undefined,
|
||||
domains: [],
|
||||
id: undefined,
|
||||
tenantId: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
describe("test render", () => {
|
||||
it("renders with no directories", () => {
|
||||
const props = createBlankProps();
|
||||
|
||||
const wrapper = shallow(<DirectoryListComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with directories and selected", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
|
||||
props.selectedDirectoryId = "asdfghjklzxcvbnm9876543210";
|
||||
|
||||
const wrapper = shallow(<DirectoryListComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with filters", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "1234567890";
|
||||
const tenant2 = createBlankDirectory();
|
||||
tenant1.displayName = "Macrohard";
|
||||
tenant1.tenantId = "9876543210";
|
||||
props.directories = [tenant1, tenant2];
|
||||
props.selectedDirectoryId = "9876543210";
|
||||
|
||||
const wrapper = mount(<DirectoryListComponent {...props} />);
|
||||
wrapper.find("input.ms-TextField-field").simulate("change", { target: { value: "Macro" } });
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("test function", () => {
|
||||
it("on new directory selected", () => {
|
||||
const props = createBlankProps();
|
||||
const tenant1 = createBlankDirectory();
|
||||
tenant1.displayName = "Microsoft";
|
||||
tenant1.tenantId = "asdfghjklzxcvbnm1234567890";
|
||||
props.directories = [tenant1];
|
||||
|
||||
const wrapper = mount(<DirectoryListComponent {...props} />);
|
||||
wrapper.find("button.directoryListButton").simulate("click");
|
||||
expect(props.onNewDirectorySelected).toBeCalled();
|
||||
expect(props.onNewDirectorySelected).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,126 +0,0 @@
|
||||
import {
|
||||
DefaultButton,
|
||||
IButtonProps,
|
||||
ITextFieldProps,
|
||||
List,
|
||||
ScrollablePane,
|
||||
Sticky,
|
||||
StickyPositionType,
|
||||
TextField,
|
||||
} from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import _ from "underscore";
|
||||
import { Tenant } from "../../../Contracts/DataModels";
|
||||
|
||||
export interface DirectoryListProps {
|
||||
directories: Array<Tenant>;
|
||||
selectedDirectoryId: string;
|
||||
onNewDirectorySelected: (newDirectory: Tenant) => void;
|
||||
}
|
||||
|
||||
export interface DirectoryListComponentState {
|
||||
filterText: string;
|
||||
}
|
||||
|
||||
// onRenderCell is not called when selectedDirectoryId changed, so add a selected state to force render
|
||||
interface ListTenant extends Tenant {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export class DirectoryListComponent extends React.Component<DirectoryListProps, DirectoryListComponentState> {
|
||||
constructor(props: DirectoryListProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterText: "",
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { directories: originalItems, selectedDirectoryId } = this.props;
|
||||
const { filterText } = this.state;
|
||||
const filteredItems =
|
||||
originalItems && originalItems.length && filterText
|
||||
? originalItems.filter(
|
||||
(directory) =>
|
||||
directory.displayName &&
|
||||
directory.displayName.toLowerCase().indexOf(filterText && filterText.toLowerCase()) >= 0
|
||||
)
|
||||
: originalItems;
|
||||
const filteredItemsSelected = filteredItems.map((t) => {
|
||||
let tenant: ListTenant = t;
|
||||
tenant.selected = t.tenantId === selectedDirectoryId;
|
||||
return tenant;
|
||||
});
|
||||
|
||||
const textFieldProps: ITextFieldProps = {
|
||||
className: "directoryListFilterTextBox",
|
||||
placeholder: "Filter by directory name",
|
||||
onChange: this._onFilterChanged,
|
||||
ariaLabel: "Directory filter text box",
|
||||
};
|
||||
|
||||
// TODO: add magnify glass to search bar with onRenderSuffix
|
||||
return (
|
||||
<ScrollablePane data-is-scrollable="true">
|
||||
<Sticky stickyPosition={StickyPositionType.Header}>
|
||||
<TextField {...textFieldProps} />
|
||||
</Sticky>
|
||||
<List items={filteredItemsSelected} onRenderCell={this._onRenderCell} />
|
||||
</ScrollablePane>
|
||||
);
|
||||
}
|
||||
|
||||
private _onFilterChanged = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text?: string): void => {
|
||||
this.setState({
|
||||
filterText: text,
|
||||
});
|
||||
};
|
||||
|
||||
private _onRenderCell = (directory: ListTenant): JSX.Element => {
|
||||
const buttonProps: IButtonProps = {
|
||||
disabled: directory.selected || false,
|
||||
className: "directoryListButton",
|
||||
onClick: this._onNewDirectoryClick,
|
||||
styles: {
|
||||
root: {
|
||||
backgroundColor: "transparent",
|
||||
height: "auto",
|
||||
borderBottom: "1px solid #ccc",
|
||||
padding: "1px 0",
|
||||
width: "100%",
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: "#f1f1f8",
|
||||
},
|
||||
rootHovered: {
|
||||
backgroundColor: "rgba(85,179,255,.1)",
|
||||
},
|
||||
flexContainer: {
|
||||
height: "auto",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<DefaultButton {...buttonProps}>
|
||||
<div className="directoryListItem" data-is-focusable={true}>
|
||||
<div className="directoryListItemName">{directory.displayName}</div>
|
||||
<div className="directoryListItemId">{directory.tenantId}</div>
|
||||
</div>
|
||||
</DefaultButton>
|
||||
);
|
||||
};
|
||||
|
||||
private _onNewDirectoryClick = (e: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
if (!e || !e.currentTarget) {
|
||||
return;
|
||||
}
|
||||
const buttonElement = e.currentTarget;
|
||||
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
|
||||
const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === selectedDirectoryId);
|
||||
|
||||
this.props.onNewDirectorySelected(selectedDirectory);
|
||||
};
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`test render renders with directories and default 1`] = `
|
||||
<Dropdown
|
||||
className="defaultDirectoryDropdown"
|
||||
defaultSelectedKey="asdfghjklzxcvbnm9876543210"
|
||||
label="Set your default directory"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "lastVisited",
|
||||
"text": "Sign in to your last visited directory",
|
||||
},
|
||||
Object {
|
||||
"key": "asdfghjklzxcvbnm9876543210",
|
||||
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
|
||||
},
|
||||
Object {
|
||||
"key": "",
|
||||
"text": "()",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test render renders with directories and last visit default 1`] = `
|
||||
<Dropdown
|
||||
className="defaultDirectoryDropdown"
|
||||
defaultSelectedKey="lastVisited"
|
||||
label="Set your default directory"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "lastVisited",
|
||||
"text": "Sign in to your last visited directory",
|
||||
},
|
||||
Object {
|
||||
"key": "asdfghjklzxcvbnm9876543210",
|
||||
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
|
||||
},
|
||||
Object {
|
||||
"key": "",
|
||||
"text": "()",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test render renders with directories but no default 1`] = `
|
||||
<Dropdown
|
||||
className="defaultDirectoryDropdown"
|
||||
defaultSelectedKey="lastVisited"
|
||||
label="Set your default directory"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "lastVisited",
|
||||
"text": "Sign in to your last visited directory",
|
||||
},
|
||||
Object {
|
||||
"key": "asdfghjklzxcvbnm9876543210",
|
||||
"text": "Macrohard(asdfghjklzxcvbnm9876543210)",
|
||||
},
|
||||
Object {
|
||||
"key": "",
|
||||
"text": "()",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test render renders with no directories 1`] = `
|
||||
<Dropdown
|
||||
className="defaultDirectoryDropdown"
|
||||
defaultSelectedKey="lastVisited"
|
||||
label="Set your default directory"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "lastVisited",
|
||||
"text": "Sign in to your last visited directory",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
||||
@import "../../../../less/Common/Constants.less";
|
||||
|
||||
.radioSwitchComponent {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
|
||||
&>span:nth-child(n+2) {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
color: @BaseDark;
|
||||
padding-left: @SmallSpace;
|
||||
vertical-align: top;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* Horizontal switch component
|
||||
*/
|
||||
|
||||
import { Icon } from "@fluentui/react";
|
||||
import * as React from "react";
|
||||
import { NormalizedEventKey } from "../../../Common/Constants";
|
||||
import "./RadioSwitchComponent.less";
|
||||
|
||||
export interface Choice {
|
||||
key: string;
|
||||
onSelect: () => void;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface RadioSwitchComponentProps {
|
||||
choices: Choice[];
|
||||
selectedKey: string;
|
||||
onSelectionKeyChange?: (newValue: string) => void;
|
||||
}
|
||||
|
||||
export class RadioSwitchComponent extends React.Component<RadioSwitchComponentProps> {
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<div className="radioSwitchComponent">
|
||||
{this.props.choices.map((choice: Choice) => (
|
||||
<span
|
||||
tabIndex={0}
|
||||
key={choice.key}
|
||||
onClick={() => this.onSelect(choice)}
|
||||
onKeyPress={(event) => this.onKeyPress(event, choice)}
|
||||
>
|
||||
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
|
||||
<span className="caption">{choice.label}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onSelect(choice: Choice): void {
|
||||
this.props.onSelectionKeyChange && this.props.onSelectionKeyChange(choice.key);
|
||||
choice.onSelect();
|
||||
}
|
||||
|
||||
private onKeyPress(event: React.KeyboardEvent<HTMLSpanElement>, choice: Choice): void {
|
||||
if (event.key === NormalizedEventKey.Enter || event.key === NormalizedEventKey.Space) {
|
||||
this.onSelect(choice);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Generic abstract React component that senses its dimensions.
|
||||
* It updates its state and re-renders if dimensions change.
|
||||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import * as ResizeSensor from "css-element-queries/src/ResizeSensor";
|
||||
|
||||
export abstract class ResizeSensorComponent<P, S> extends React.Component<P, S> {
|
||||
private isSensing: boolean = false;
|
||||
private resizeSensor: any;
|
||||
|
||||
public constructor(props: P) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
protected abstract onDimensionsChanged(width: number, height: number): void;
|
||||
protected abstract getSensorTarget(): HTMLElement;
|
||||
|
||||
public componentDidUpdate(): void {
|
||||
if (this.isSensing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bar = this.getSensorTarget();
|
||||
if (bar.clientWidth > 0 || bar.clientHeight > 0) {
|
||||
const oldPosition = bar.style.position;
|
||||
// TODO Find a better way to use constructor
|
||||
this.resizeSensor = new (ResizeSensor as any)(bar, () => {
|
||||
this.onDimensionsChanged(bar.clientWidth, bar.clientHeight);
|
||||
});
|
||||
this.isSensing = true;
|
||||
|
||||
// ResizeSensor.js sets position to 'relative' which makes the dropdown menu appear clipped.
|
||||
// Undoing doesn't seem to affect resize sensing functionality.
|
||||
bar.style.position = oldPosition;
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount(): void {
|
||||
if (!!this.resizeSensor) {
|
||||
this.resizeSensor.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,10 +202,12 @@ export class ScaleComponent extends React.Component<ScaleComponentProps> {
|
||||
}
|
||||
|
||||
private getFreeTierInfoMessage(): JSX.Element {
|
||||
const freeTierLimits = SharedConstants.FreeTierLimits;
|
||||
return (
|
||||
<Text>
|
||||
With free tier, you will get the first 400 RU/s and 5 GB of storage in this account for free. To keep your
|
||||
account free, keep the total RU/s across all resources in the account to 400 RU/s.
|
||||
With free tier, you will get the first {freeTierLimits.RU} RU/s and {freeTierLimits.Storage} GB of storage in
|
||||
this account for free. To keep your account free, keep the total RU/s across all resources in the account to{" "}
|
||||
{freeTierLimits.RU} RU/s.
|
||||
<Link
|
||||
href="https://docs.microsoft.com/en-us/azure/cosmos-db/understand-your-bill#billing-examples-with-free-tier-accounts"
|
||||
target="_blank"
|
||||
|
||||
@@ -155,7 +155,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
this.state = {
|
||||
spendAckChecked: this.props.spendAckChecked,
|
||||
exceedFreeTierThroughput:
|
||||
this.props.isFreeTierAccount && !this.props.isAutoPilotSelected && this.props.throughput > 400,
|
||||
this.props.isFreeTierAccount &&
|
||||
!this.props.isAutoPilotSelected &&
|
||||
this.props.throughput > SharedConstants.FreeTierLimits.RU,
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
@@ -441,7 +443,9 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
if (this.overrideWithAutoPilotSettings()) {
|
||||
this.props.onMaxAutoPilotThroughputChange(newThroughput);
|
||||
} else {
|
||||
this.setState({ exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > 400 });
|
||||
this.setState({
|
||||
exceedFreeTierThroughput: this.props.isFreeTierAccount && newThroughput > SharedConstants.FreeTierLimits.RU,
|
||||
});
|
||||
this.props.onThroughputChange(newThroughput);
|
||||
}
|
||||
};
|
||||
@@ -581,9 +585,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
messageBarIconProps={{ iconName: "WarningSolid", className: "messageBarWarningIcon" }}
|
||||
styles={messageBarStyles}
|
||||
>
|
||||
{
|
||||
"Billing will apply if you provision more than 400 RU/s of manual throughput, or if the resource scales beyond 400 RU/s with autoscale."
|
||||
}
|
||||
{`Billing will apply if you provision more than ${SharedConstants.FreeTierLimits.RU} RU/s of manual throughput, or if the resource scales beyond ${SharedConstants.FreeTierLimits.RU} RU/s with autoscale.`}
|
||||
</MessageBar>
|
||||
)}
|
||||
{this.props.getThroughputWarningMessage() && (
|
||||
|
||||
@@ -187,9 +187,8 @@ export const ThroughputInput: FunctionComponent<ThroughputInputProps> = ({
|
||||
<TooltipHost
|
||||
directionalHint={DirectionalHint.topLeftEdge}
|
||||
content={
|
||||
showFreeTierExceedThroughputTooltip &&
|
||||
throughput > SharedConstants.CollectionCreation.DefaultCollectionRUs400
|
||||
? "The first 400 RU/s in this account are free. Billing will apply to any throughput beyond 400 RU/s."
|
||||
showFreeTierExceedThroughputTooltip && throughput > SharedConstants.FreeTierLimits.RU
|
||||
? `The first ${SharedConstants.FreeTierLimits.RU} RU/s in this account are free. Billing will apply to any throughput beyond ${SharedConstants.FreeTierLimits.RU} RU/s.`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import * as ko from "knockout";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import ThroughputInputComponentAutoscaleV3 from "./ThroughputInputComponentAutoscaleV3.html";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { FreeTierLimits } from "../../../Shared/Constants";
|
||||
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import * as AutoPilotUtils from "../../../Utils/AutoPilotUtils";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import ThroughputInputComponentAutoscaleV3 from "./ThroughputInputComponentAutoscaleV3.html";
|
||||
|
||||
/**
|
||||
* Throughput Input:
|
||||
*
|
||||
@@ -236,11 +236,11 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
this.freeTierExceedThroughputTooltip = options.freeTierExceedThroughputTooltip || ko.observable<string>();
|
||||
this.freeTierExceedThroughputWarning = options.freeTierExceedThroughputWarning || ko.observable<string>();
|
||||
this.showFreeTierExceedThroughputTooltip = ko.pureComputed<boolean>(
|
||||
() => !!this.freeTierExceedThroughputTooltip() && this.value() > 400
|
||||
() => !!this.freeTierExceedThroughputTooltip() && this.value() > FreeTierLimits.RU
|
||||
);
|
||||
|
||||
this.showFreeTierExceedThroughputWarning = ko.pureComputed<boolean>(
|
||||
() => !!this.freeTierExceedThroughputWarning() && this.value() > 400
|
||||
() => !!this.freeTierExceedThroughputWarning() && this.value() > FreeTierLimits.RU
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface GremlinSimpleClientParameters {
|
||||
password: string;
|
||||
successCallback: (result: Result) => void;
|
||||
progressCallback: (result: Result) => void;
|
||||
failureCallback: (result: Result, error: string) => void;
|
||||
failureCallback: (result: Result | null, error: string) => void;
|
||||
infoCallback: (msg: string) => void;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ export class GremlinSimpleClient {
|
||||
private static readonly requestChargeHeader = "x-ms-request-charge";
|
||||
|
||||
public params: GremlinSimpleClientParameters;
|
||||
private protocols: string | string[];
|
||||
private ws: WebSocket;
|
||||
|
||||
public requestsToSend: { [requestId: string]: GremlinRequestMessage };
|
||||
@@ -72,6 +71,7 @@ export class GremlinSimpleClient {
|
||||
this.params = params;
|
||||
this.pendingRequests = {};
|
||||
this.requestsToSend = {};
|
||||
this.ws = GremlinSimpleClient.createWebSocket(this.params.endpoint);
|
||||
}
|
||||
|
||||
public connect() {
|
||||
@@ -117,7 +117,7 @@ export class GremlinSimpleClient {
|
||||
}
|
||||
}
|
||||
|
||||
public decodeMessage(msg: MessageEvent): GremlinResponseMessage {
|
||||
public decodeMessage(msg: MessageEvent): GremlinResponseMessage | null {
|
||||
if (msg.data === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ export class GremlinSimpleClient {
|
||||
|
||||
public static utf8ToB64(utf8Str: string) {
|
||||
return btoa(
|
||||
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
||||
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, function (_match, p1) {
|
||||
return String.fromCharCode(parseInt(p1, 16));
|
||||
})
|
||||
);
|
||||
@@ -305,7 +305,7 @@ export class GremlinSimpleClient {
|
||||
return binaryMessage;
|
||||
}
|
||||
|
||||
private onOpen(event: any) {
|
||||
private onOpen(_event: any) {
|
||||
this.executeRequestsToSend();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
/* eslint jsx-a11y/no-static-element-interactions: 0 */
|
||||
/* eslint jsx-a11y/click-events-have-key-events: 0 */
|
||||
|
||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { actions, selectors, ContentRef, AppState } from "@nteract/core";
|
||||
|
||||
interface ComponentProps {
|
||||
id: string;
|
||||
contentRef: ContentRef;
|
||||
@@ -70,7 +69,7 @@ export class HijackScroll extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
const makeMapStateToProps = (initialState: AppState, ownProps: ComponentProps) => {
|
||||
const makeMapStateToProps = (_initialState: AppState, ownProps: ComponentProps) => {
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const { id, contentRef } = ownProps;
|
||||
const model = selectors.model(state, { contentRef });
|
||||
@@ -87,7 +86,7 @@ const makeMapStateToProps = (initialState: AppState, ownProps: ComponentProps) =
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const makeMapDispatchToProps = (initialDispatch: Dispatch, ownProps: ComponentProps) => {
|
||||
const makeMapDispatchToProps = (_initialDispatch: Dispatch, ownProps: ComponentProps) => {
|
||||
const mapDispatchToProps = (dispatch: Dispatch) => ({
|
||||
selectCell: () => dispatch(actions.focusCell({ id: ownProps.id, contentRef: ownProps.contentRef })),
|
||||
});
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||
import Immutable from "immutable";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Dispatch } from "redux";
|
||||
|
||||
import { CellId } from "@nteract/commutable";
|
||||
import { actions, AppState, ContentRef, selectors } from "@nteract/core";
|
||||
|
||||
interface ComponentProps {
|
||||
contentRef: ContentRef;
|
||||
children: React.ReactNode;
|
||||
@@ -107,7 +106,7 @@ export class KeyboardShortcuts extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export const makeMapStateToProps = (state: AppState, ownProps: ComponentProps) => {
|
||||
export const makeMapStateToProps = (_state: AppState, ownProps: ComponentProps) => {
|
||||
const { contentRef } = ownProps;
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
const model = selectors.model(state, { contentRef });
|
||||
|
||||
@@ -36,7 +36,7 @@ export const PanelInfoErrorComponent: React.FunctionComponent<PanelInfoErrorProp
|
||||
{message}
|
||||
{link && linkText && (
|
||||
<Link target="_blank" href={link}>
|
||||
{linkText}
|
||||
{" " + linkText}
|
||||
</Link>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
@@ -17,6 +17,8 @@ import * as _ from "underscore";
|
||||
import { sendMessage } from "../Common/MessageHandler";
|
||||
import { SelfServeMessageTypes } from "../Contracts/SelfServeContracts";
|
||||
import { SmartUiComponent, SmartUiDescriptor } from "../Explorer/Controls/SmartUi/SmartUiComponent";
|
||||
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
|
||||
import { trace } from "../Shared/Telemetry/TelemetryProcessor";
|
||||
import { commandBarItemStyles, commandBarStyles, containerStackTokens, separatorStyles } from "./SelfServeStyles";
|
||||
import {
|
||||
AnyDisplay,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
Node,
|
||||
NumberInput,
|
||||
RefreshResult,
|
||||
SelfServeComponentTelemetryType,
|
||||
SelfServeDescriptor,
|
||||
SmartUiInput,
|
||||
StringInput,
|
||||
@@ -87,6 +90,12 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
}
|
||||
});
|
||||
this.initializeSmartUiComponent();
|
||||
|
||||
const telemetryData = {
|
||||
selfServeClassName: this.props.descriptor.root.id,
|
||||
eventType: SelfServeComponentTelemetryType.Load,
|
||||
};
|
||||
trace(Action.SelfServeComponent, ActionModifiers.Mark, telemetryData, SelfServeMessageTypes.TelemetryInfo);
|
||||
}
|
||||
|
||||
constructor(props: SelfServeComponentProps) {
|
||||
@@ -262,6 +271,12 @@ export class SelfServeComponent extends React.Component<SelfServeComponentProps,
|
||||
};
|
||||
|
||||
public performSave = async (): Promise<void> => {
|
||||
const telemetryData = {
|
||||
selfServeClassName: this.props.descriptor.root.id,
|
||||
eventType: SelfServeComponentTelemetryType.Save,
|
||||
};
|
||||
trace(Action.SelfServeComponent, ActionModifiers.Mark, telemetryData, SelfServeMessageTypes.TelemetryInfo);
|
||||
|
||||
this.setState({ isSaving: true, notification: undefined });
|
||||
try {
|
||||
const onSaveResult = await this.props.descriptor.onSave(
|
||||
|
||||
@@ -70,6 +70,12 @@ export interface SelfServeDescriptor {
|
||||
refreshParams?: RefreshParams;
|
||||
}
|
||||
|
||||
/**@internal */
|
||||
export enum SelfServeComponentTelemetryType {
|
||||
Load = "Load",
|
||||
Save = "Save",
|
||||
}
|
||||
|
||||
/**@internal */
|
||||
export type AnyDisplay = NumberInput | BooleanInput | StringInput | ChoiceInput | DescriptionDisplay;
|
||||
|
||||
|
||||
@@ -201,3 +201,8 @@ export class SubscriptionUtilMappings {
|
||||
export class AutopilotDocumentation {
|
||||
public static Url: string = "https://aka.ms/cosmos-autoscale-info";
|
||||
}
|
||||
|
||||
export class FreeTierLimits {
|
||||
public static RU: number = 1000;
|
||||
public static Storage: number = 25;
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ export enum Action {
|
||||
SelfServe,
|
||||
ExpandAddCollectionPaneAdvancedSection,
|
||||
SchemaAnalyzerClickAnalyze,
|
||||
SelfServeComponent,
|
||||
}
|
||||
|
||||
export const ActionModifiers = {
|
||||
|
||||
@@ -267,9 +267,11 @@ export function getUpsellMessage(
|
||||
if (isFreeTier) {
|
||||
const collectionName = getCollectionName().toLocaleLowerCase();
|
||||
const resourceType = isCollection ? collectionName : "database";
|
||||
const freeTierMaxRU = Constants.FreeTierLimits.RU;
|
||||
const freeTierMaxStorage = Constants.FreeTierLimits.Storage;
|
||||
return isFirstResourceCreated
|
||||
? `The free tier discount of 400 RU/s has already been applied to a database or ${collectionName} in this account. Billing will apply to this ${resourceType} after it is created.`
|
||||
: `With free tier, you'll get the first 400 RU/s and 5 GB of storage in this account for free. Billing will apply if you provision more than 400 RU/s of manual throughput, or if the ${resourceType} scales beyond 400 RU/s with autoscale.`;
|
||||
? `Your account currently has at least 1 database or ${collectionName} with provisioned RU/s. Billing will apply to this ${resourceType} if the total RU/s in your account exceeds ${freeTierMaxRU} RU/s.`
|
||||
: `With free tier, you'll get the first ${freeTierMaxRU} RU/s and ${freeTierMaxStorage} GB of storage in this account for free. Billing will apply if you provision more than ${freeTierMaxRU} RU/s of manual throughput, or if the ${resourceType} scales beyond ${freeTierMaxRU} RU/s with autoscale.`;
|
||||
} else {
|
||||
let price: number = Constants.OfferPricing.MonthlyPricing.default.Standard.StartingPrice;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import { ConfigContext, initializeConfiguration } from "../ConfigContext";
|
||||
// This hook initializes global configuration from a config.json file that is injected at deploy time
|
||||
// This allows the same main Data Explorer build to be exactly the same in all clouds/platforms,
|
||||
// but override some of the configuration as nesssary
|
||||
export function useConfig(): Readonly<ConfigContext> {
|
||||
const [state, setState] = useState<ConfigContext>();
|
||||
export function useConfig(): Readonly<ConfigContext | undefined> {
|
||||
const [state, setState] = useState<ConfigContext | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
initializeConfiguration().then((response) => setState(response));
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"noUnusedParameters": true
|
||||
},
|
||||
"files": [
|
||||
"src/Explorer/Graph/GraphExplorerComponent/GremlinSimpleClient.ts",
|
||||
"./src/AuthType.ts",
|
||||
"./src/Bindings/ReactBindingHandler.ts",
|
||||
"./src/Common/ArrayHashMap.ts",
|
||||
@@ -114,6 +115,7 @@
|
||||
"./src/Utils/StyleUtils.ts",
|
||||
"./src/Utils/WindowUtils.test.ts",
|
||||
"./src/Utils/WindowUtils.ts",
|
||||
"./src/hooks/useConfig.ts",
|
||||
"./src/hooks/useDirectories.tsx",
|
||||
"./src/hooks/useFullScreenURLs.tsx",
|
||||
"./src/hooks/useGraphPhoto.tsx",
|
||||
@@ -121,7 +123,8 @@
|
||||
"./src/i18n.ts",
|
||||
"./src/quickstart.ts",
|
||||
"./src/setupTests.ts",
|
||||
"./src/userContext.test.ts"
|
||||
"./src/userContext.test.ts",
|
||||
"src/Common/EntityValue.tsx"
|
||||
],
|
||||
"include": [
|
||||
"src/CellOutputViewer/transforms/**/*",
|
||||
@@ -135,6 +138,8 @@
|
||||
"src/Explorer/Controls/ResizeSensorReactComponent/**/*",
|
||||
"src/Explorer/Graph/GraphExplorerComponent/__mocks__/**/*",
|
||||
"src/Explorer/Notebook/NotebookComponent/__mocks__/**/*",
|
||||
"src/Explorer/Notebook/NotebookRenderer/decorators/hijack-scroll/**/*",
|
||||
"src/Explorer/Notebook/NotebookRenderer/decorators/kbd-shortcuts/**/*",
|
||||
"src/Explorer/Panes/RightPaneForm/**/*",
|
||||
"src/Libs/**/*",
|
||||
"src/Localization/**/*",
|
||||
@@ -144,4 +149,4 @@
|
||||
"src/Terminal/**/*",
|
||||
"src/Utils/arm/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user