mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-20 01:11:25 +00:00
Prettier 2.0 (#393)
This commit is contained in:
@@ -31,7 +31,7 @@ export class AccessibleElement extends React.Component<AccessibleElementProps> {
|
||||
...elementProps,
|
||||
onKeyPress: this.onKeyPress,
|
||||
onClick: this.props.onActivated,
|
||||
tabIndex
|
||||
tabIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
super(props);
|
||||
this.isExpanded = props.isExpanded;
|
||||
this.state = {
|
||||
isExpanded: true
|
||||
isExpanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export class AccordionItemComponent extends React.Component<AccordionItemCompone
|
||||
if (this.props.isExpanded !== this.isExpanded) {
|
||||
this.isExpanded = this.props.isExpanded;
|
||||
this.setState({
|
||||
isExpanded: this.props.isExpanded
|
||||
isExpanded: this.props.isExpanded,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
constructor(props: ArcadiaMenuPickerProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedSparkPool: props.selectedSparkPool
|
||||
selectedSparkPool: props.selectedSparkPool,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
try {
|
||||
this.props.onSparkPoolSelect(e, item);
|
||||
this.setState({
|
||||
selectedSparkPool: item.text
|
||||
selectedSparkPool: item.text,
|
||||
});
|
||||
} catch (error) {
|
||||
Logger.logError(getErrorMessage(error), "ArcadiaMenuPicker/_onSparkPoolClicked");
|
||||
@@ -65,28 +65,28 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
|
||||
public render() {
|
||||
const { workspaces } = this.props;
|
||||
let workspaceMenuItems: IContextualMenuItem[] = workspaces.map(workspace => {
|
||||
let workspaceMenuItems: IContextualMenuItem[] = workspaces.map((workspace) => {
|
||||
let sparkPoolsMenuProps: IContextualMenuProps = {
|
||||
items: workspace.sparkPools.map(
|
||||
(sparkpool): IContextualMenuItem => ({
|
||||
key: sparkpool.id,
|
||||
text: sparkpool.name,
|
||||
onClick: this._onSparkPoolClicked
|
||||
onClick: this._onSparkPoolClicked,
|
||||
})
|
||||
)
|
||||
),
|
||||
};
|
||||
if (!sparkPoolsMenuProps.items.length) {
|
||||
sparkPoolsMenuProps.items.push({
|
||||
key: workspace.id,
|
||||
text: "Create new spark pool",
|
||||
onClick: this._onCreateNewSparkPoolClicked
|
||||
onClick: this._onCreateNewSparkPoolClicked,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
key: workspace.id,
|
||||
text: workspace.name,
|
||||
subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps
|
||||
subMenuProps: this.props.disableSubmenu ? undefined : sparkPoolsMenuProps,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -94,7 +94,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
workspaceMenuItems.push({
|
||||
key: "create_workspace",
|
||||
text: "Create new workspace",
|
||||
onClick: this._onCreateNewWorkspaceClicked
|
||||
onClick: this._onCreateNewWorkspaceClicked,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,29 +103,29 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
backgroundColor: "transparent",
|
||||
margin: "auto 5px",
|
||||
padding: "0",
|
||||
border: "0"
|
||||
border: "0",
|
||||
},
|
||||
rootHovered: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
rootChecked: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
rootFocused: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
rootExpanded: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
flexContainer: {
|
||||
height: "30px",
|
||||
border: "1px solid #a6a6a6",
|
||||
padding: "0 8px"
|
||||
padding: "0 8px",
|
||||
},
|
||||
label: {
|
||||
fontWeight: "400",
|
||||
fontSize: "12px"
|
||||
}
|
||||
fontSize: "12px",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -134,7 +134,7 @@ export class ArcadiaMenuPicker extends React.Component<ArcadiaMenuPickerProps, A
|
||||
persistMenu={true}
|
||||
className="arcadia-menu-picker"
|
||||
menuProps={{
|
||||
items: workspaceMenuItems
|
||||
items: workspaceMenuItems,
|
||||
}}
|
||||
styles={dropdownStyle}
|
||||
/>
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import * as ko from "knockout";
|
||||
import template from "./collapsible-panel-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class CollapsiblePanelComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: CollapsiblePanelViewModel,
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface CollapsiblePanelParams {
|
||||
collapsedTitle: ko.Observable<string>;
|
||||
expandedTitle: ko.Observable<string>;
|
||||
isCollapsed?: ko.Observable<boolean>;
|
||||
collapseToLeft?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible panel:
|
||||
* Contains a header with [>] button to collapse and an title ("expandedTitle").
|
||||
* Collapsing the panel:
|
||||
* - shrinks width to narrow amount
|
||||
* - hides children
|
||||
* - shows [<]
|
||||
* - shows vertical title ("collapsedTitle")
|
||||
* - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter)
|
||||
*
|
||||
* How to use in your markup:
|
||||
* <collapsible-panel params="{ collapsedTitle:'Properties', expandedTitle:'Expanded properties' }">
|
||||
* <!-- add your markup here: the ko context is the same as outside of collapsible-panel (ie $data) -->
|
||||
* </collapsible-panel>
|
||||
*
|
||||
* Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component.
|
||||
* Use the optional "collapseToLeft" parameter to collapse to the left.
|
||||
*/
|
||||
class CollapsiblePanelViewModel {
|
||||
public params: CollapsiblePanelParams;
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
|
||||
public constructor(params: CollapsiblePanelParams) {
|
||||
this.params = params;
|
||||
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
||||
}
|
||||
|
||||
public toggleCollapse(): void {
|
||||
this.isCollapsed(!this.isCollapsed());
|
||||
}
|
||||
}
|
||||
import * as ko from "knockout";
|
||||
import template from "./collapsible-panel-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class CollapsiblePanelComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: CollapsiblePanelViewModel,
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface CollapsiblePanelParams {
|
||||
collapsedTitle: ko.Observable<string>;
|
||||
expandedTitle: ko.Observable<string>;
|
||||
isCollapsed?: ko.Observable<boolean>;
|
||||
collapseToLeft?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible panel:
|
||||
* Contains a header with [>] button to collapse and an title ("expandedTitle").
|
||||
* Collapsing the panel:
|
||||
* - shrinks width to narrow amount
|
||||
* - hides children
|
||||
* - shows [<]
|
||||
* - shows vertical title ("collapsedTitle")
|
||||
* - the default behavior is to collapse to the right (ie, place this component on the right or use "collapseToLeft" parameter)
|
||||
*
|
||||
* How to use in your markup:
|
||||
* <collapsible-panel params="{ collapsedTitle:'Properties', expandedTitle:'Expanded properties' }">
|
||||
* <!-- add your markup here: the ko context is the same as outside of collapsible-panel (ie $data) -->
|
||||
* </collapsible-panel>
|
||||
*
|
||||
* Use the optional "isCollapsed" parameter to programmatically collapse/expand the pane from outside the component.
|
||||
* Use the optional "collapseToLeft" parameter to collapse to the left.
|
||||
*/
|
||||
class CollapsiblePanelViewModel {
|
||||
public params: CollapsiblePanelParams;
|
||||
private isCollapsed: ko.Observable<boolean>;
|
||||
|
||||
public constructor(params: CollapsiblePanelParams) {
|
||||
this.params = params;
|
||||
this.isCollapsed = params.isCollapsed || ko.observable(false);
|
||||
}
|
||||
|
||||
public toggleCollapse(): void {
|
||||
this.isCollapsed(!this.isCollapsed());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CollapsibleSectionComponent, CollapsibleSectionProps } from "./Collapsi
|
||||
describe("CollapsibleSectionComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: CollapsibleSectionProps = {
|
||||
title: "Sample title"
|
||||
title: "Sample title",
|
||||
};
|
||||
|
||||
const wrapper = shallow(<CollapsibleSectionComponent {...props} />);
|
||||
|
||||
@@ -14,7 +14,7 @@ export class CollapsibleSectionComponent extends React.Component<CollapsibleSect
|
||||
constructor(props: CollapsibleSectionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExpanded: true
|
||||
isExpanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
<div class="collapsiblePanel" data-bind="css: { paneCollapsed:isCollapsed() }">
|
||||
<div class="panelHeader" data-bind="visible: !isCollapsed()">
|
||||
<span
|
||||
class="collapsedIconContainer collapseExpandButton"
|
||||
data-bind="click:toggleCollapse, css: { 'pull-right':params.collapseToLeft }"
|
||||
>
|
||||
<img
|
||||
class="collapsedIcon imgVerticalAlignment"
|
||||
src="/imgarrowlefticon.svg"
|
||||
alt="Collapse"
|
||||
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="expandedTitle"
|
||||
data-bind="text: params.expandedTitle, css:{ iconSpacer:!params.collapseToLeft }"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="collapsibleNav nav" data-bind="visible:isCollapsed">
|
||||
<ul class="nav">
|
||||
<li class="collapsedBtn collapseExpandButton">
|
||||
<span class="collapsedIconContainer" data-bind="click: toggleCollapse">
|
||||
<img
|
||||
class="collapsedIcon"
|
||||
src="/imgarrowlefticon.svg"
|
||||
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
|
||||
alt="Expand"
|
||||
/>
|
||||
</span>
|
||||
<span class="rotatedInner" data-bind="click: toggleCollapse">
|
||||
<span data-bind="text: params.collapsedTitle"></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panelContent" data-bind="visible:!isCollapsed()">
|
||||
<!-- ko with:$parent -->
|
||||
<!-- ko template: { nodes: $componentTemplateNodes } -->
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapsiblePanel" data-bind="css: { paneCollapsed:isCollapsed() }">
|
||||
<div class="panelHeader" data-bind="visible: !isCollapsed()">
|
||||
<span
|
||||
class="collapsedIconContainer collapseExpandButton"
|
||||
data-bind="click:toggleCollapse, css: { 'pull-right':params.collapseToLeft }"
|
||||
>
|
||||
<img
|
||||
class="collapsedIcon imgVerticalAlignment"
|
||||
src="/imgarrowlefticon.svg"
|
||||
alt="Collapse"
|
||||
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="expandedTitle"
|
||||
data-bind="text: params.expandedTitle, css:{ iconSpacer:!params.collapseToLeft }"
|
||||
></span>
|
||||
</div>
|
||||
|
||||
<div class="collapsibleNav nav" data-bind="visible:isCollapsed">
|
||||
<ul class="nav">
|
||||
<li class="collapsedBtn collapseExpandButton">
|
||||
<span class="collapsedIconContainer" data-bind="click: toggleCollapse">
|
||||
<img
|
||||
class="collapsedIcon"
|
||||
src="/imgarrowlefticon.svg"
|
||||
data-bind="css: { expanded:!isCollapsed(), iconMirror:params.collapseToLeft }"
|
||||
alt="Expand"
|
||||
/>
|
||||
</span>
|
||||
<span class="rotatedInner" data-bind="click: toggleCollapse">
|
||||
<span data-bind="text: params.collapsedTitle"></span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="panelContent" data-bind="visible:!isCollapsed()">
|
||||
<!-- ko with:$parent -->
|
||||
<!-- ko template: { nodes: $componentTemplateNodes } -->
|
||||
<!-- /ko -->
|
||||
<!-- /ko -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -149,9 +149,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
|
||||
private onLauncherKeyDown(event: React.KeyboardEvent<HTMLDivElement>): boolean {
|
||||
if (event.keyCode === KeyCodes.DownArrow) {
|
||||
$(this.dropdownElt).hide();
|
||||
$(this.dropdownElt)
|
||||
.show()
|
||||
.focus();
|
||||
$(this.dropdownElt).show().focus();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
@@ -187,7 +185,7 @@ export class CommandButtonComponent extends React.Component<CommandButtonCompone
|
||||
}
|
||||
this.props.onCommandClick(e);
|
||||
TelemetryProcessor.trace(Action.SelectItem, ActionModifiers.Mark, {
|
||||
commandButtonClicked: this.props.commandButtonLabel
|
||||
commandButtonClicked: this.props.commandButtonLabel,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,101 +1,101 @@
|
||||
import * as React from "react";
|
||||
import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
|
||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
label: string;
|
||||
multiline: boolean;
|
||||
autoAdjustHeight: boolean;
|
||||
rows: number;
|
||||
onChange: (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface LinkProps {
|
||||
linkText: string;
|
||||
linkUrl: string;
|
||||
}
|
||||
|
||||
export interface DialogProps {
|
||||
title: string;
|
||||
subText: string;
|
||||
isModal: boolean;
|
||||
visible: boolean;
|
||||
choiceGroupProps?: IChoiceGroupProps;
|
||||
textFieldProps?: TextFieldProps;
|
||||
linkProps?: LinkProps;
|
||||
primaryButtonText: string;
|
||||
secondaryButtonText: string;
|
||||
onPrimaryButtonClick: () => void;
|
||||
onSecondaryButtonClick: () => void;
|
||||
primaryButtonDisabled?: boolean;
|
||||
type?: DialogType;
|
||||
showCloseButton?: boolean;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const DIALOG_MIN_WIDTH = "400px";
|
||||
const DIALOG_MAX_WIDTH = "600px";
|
||||
const DIALOG_TITLE_FONT_SIZE = "17px";
|
||||
const DIALOG_TITLE_FONT_WEIGHT = 400;
|
||||
const DIALOG_SUBTEXT_FONT_SIZE = "15px";
|
||||
|
||||
export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const dialogProps: IDialogProps = {
|
||||
hidden: !this.props.visible,
|
||||
dialogContentProps: {
|
||||
type: this.props.type || DialogType.normal,
|
||||
title: this.props.title,
|
||||
subText: this.props.subText,
|
||||
styles: {
|
||||
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
|
||||
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
|
||||
},
|
||||
showCloseButton: this.props.showCloseButton || false,
|
||||
onDismiss: this.props.onDismiss
|
||||
},
|
||||
modalProps: { isBlocking: this.props.isModal },
|
||||
minWidth: DIALOG_MIN_WIDTH,
|
||||
maxWidth: DIALOG_MAX_WIDTH
|
||||
};
|
||||
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
||||
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
||||
const linkProps: LinkProps = this.props.linkProps;
|
||||
const primaryButtonProps: IButtonProps = {
|
||||
text: this.props.primaryButtonText,
|
||||
disabled: this.props.primaryButtonDisabled || false,
|
||||
onClick: this.props.onPrimaryButtonClick
|
||||
};
|
||||
const secondaryButtonProps: IButtonProps =
|
||||
this.props.secondaryButtonText && this.props.onSecondaryButtonClick
|
||||
? {
|
||||
text: this.props.secondaryButtonText,
|
||||
onClick: this.props.onSecondaryButtonClick
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
{textFieldProps && <TextField {...textFieldProps} />}
|
||||
{linkProps && (
|
||||
<Link href={linkProps.linkUrl} target="_blank">
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<PrimaryButton {...primaryButtonProps} />
|
||||
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
import * as React from "react";
|
||||
import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric-react/lib/Dialog";
|
||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
import { ChoiceGroup, FontIcon, IChoiceGroupProps } from "office-ui-fabric-react";
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
label: string;
|
||||
multiline: boolean;
|
||||
autoAdjustHeight: boolean;
|
||||
rows: number;
|
||||
onChange: (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => void;
|
||||
defaultValue?: string;
|
||||
}
|
||||
|
||||
export interface LinkProps {
|
||||
linkText: string;
|
||||
linkUrl: string;
|
||||
}
|
||||
|
||||
export interface DialogProps {
|
||||
title: string;
|
||||
subText: string;
|
||||
isModal: boolean;
|
||||
visible: boolean;
|
||||
choiceGroupProps?: IChoiceGroupProps;
|
||||
textFieldProps?: TextFieldProps;
|
||||
linkProps?: LinkProps;
|
||||
primaryButtonText: string;
|
||||
secondaryButtonText: string;
|
||||
onPrimaryButtonClick: () => void;
|
||||
onSecondaryButtonClick: () => void;
|
||||
primaryButtonDisabled?: boolean;
|
||||
type?: DialogType;
|
||||
showCloseButton?: boolean;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
const DIALOG_MIN_WIDTH = "400px";
|
||||
const DIALOG_MAX_WIDTH = "600px";
|
||||
const DIALOG_TITLE_FONT_SIZE = "17px";
|
||||
const DIALOG_TITLE_FONT_WEIGHT = 400;
|
||||
const DIALOG_SUBTEXT_FONT_SIZE = "15px";
|
||||
|
||||
export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
constructor(props: DialogProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const dialogProps: IDialogProps = {
|
||||
hidden: !this.props.visible,
|
||||
dialogContentProps: {
|
||||
type: this.props.type || DialogType.normal,
|
||||
title: this.props.title,
|
||||
subText: this.props.subText,
|
||||
styles: {
|
||||
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
|
||||
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE },
|
||||
},
|
||||
showCloseButton: this.props.showCloseButton || false,
|
||||
onDismiss: this.props.onDismiss,
|
||||
},
|
||||
modalProps: { isBlocking: this.props.isModal },
|
||||
minWidth: DIALOG_MIN_WIDTH,
|
||||
maxWidth: DIALOG_MAX_WIDTH,
|
||||
};
|
||||
const choiceGroupProps: IChoiceGroupProps = this.props.choiceGroupProps;
|
||||
const textFieldProps: ITextFieldProps = this.props.textFieldProps;
|
||||
const linkProps: LinkProps = this.props.linkProps;
|
||||
const primaryButtonProps: IButtonProps = {
|
||||
text: this.props.primaryButtonText,
|
||||
disabled: this.props.primaryButtonDisabled || false,
|
||||
onClick: this.props.onPrimaryButtonClick,
|
||||
};
|
||||
const secondaryButtonProps: IButtonProps =
|
||||
this.props.secondaryButtonText && this.props.onSecondaryButtonClick
|
||||
? {
|
||||
text: this.props.secondaryButtonText,
|
||||
onClick: this.props.onSecondaryButtonClick,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Dialog {...dialogProps}>
|
||||
{choiceGroupProps && <ChoiceGroup {...choiceGroupProps} />}
|
||||
{textFieldProps && <TextField {...textFieldProps} />}
|
||||
{linkProps && (
|
||||
<Link href={linkProps.linkUrl} target="_blank">
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<PrimaryButton {...primaryButtonProps} />
|
||||
{secondaryButtonProps && <DefaultButton {...secondaryButtonProps} />}
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
/**
|
||||
* This adapter is responsible to render the Dialog React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { DialogComponent, DialogProps } from "./DialogComponent";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
|
||||
export class DialogComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<DialogProps>;
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <DialogComponent {...this.parameters()} />;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This adapter is responsible to render the Dialog React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as React from "react";
|
||||
import { DialogComponent, DialogProps } from "./DialogComponent";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
|
||||
export class DialogComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<DialogProps>;
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <DialogComponent {...this.parameters()} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class DiffEditorComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: DiffEditorViewModel,
|
||||
template
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export class DiffEditorViewModel {
|
||||
lineNumbers: this.params.lineNumbers || "off",
|
||||
fontSize: 12,
|
||||
ariaLabel: this.params.ariaLabel,
|
||||
theme: this.params.theme
|
||||
theme: this.params.theme,
|
||||
};
|
||||
|
||||
if (this.params.renderSideBySide !== undefined) {
|
||||
@@ -120,7 +120,7 @@ export class DiffEditorViewModel {
|
||||
);
|
||||
diffEditor.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel
|
||||
modified: modifiedModel,
|
||||
});
|
||||
|
||||
createCallback(diffEditor);
|
||||
@@ -147,7 +147,7 @@ export class DiffEditorViewModel {
|
||||
this.observer.observe(document.body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
childList: true
|
||||
childList: true,
|
||||
});
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const createBlankProps = (): DefaultDirectoryDropdownProps => {
|
||||
return {
|
||||
defaultDirectoryId: "",
|
||||
directories: [],
|
||||
onDefaultDirectoryChange: jest.fn()
|
||||
onDefaultDirectoryChange: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const createBlankDirectory = (): Tenant => {
|
||||
displayName: "",
|
||||
domains: [],
|
||||
id: "",
|
||||
tenantId: ""
|
||||
tenantId: "",
|
||||
};
|
||||
};
|
||||
|
||||
@@ -90,27 +90,15 @@ describe("test function", () => {
|
||||
|
||||
const wrapper = mount(<DefaultDirectoryDropdownComponent {...props} />);
|
||||
|
||||
wrapper
|
||||
.find("div.defaultDirectoryDropdown")
|
||||
.find("div.ms-Dropdown")
|
||||
.simulate("click");
|
||||
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");
|
||||
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");
|
||||
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");
|
||||
wrapper.find("button.ms-Dropdown-item").at(0).simulate("click");
|
||||
expect(props.onDefaultDirectoryChange).toBeCalled();
|
||||
expect(props.onDefaultDirectoryChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -19,13 +19,13 @@ export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDi
|
||||
public render(): JSX.Element {
|
||||
const lastVisitedOption: IDropdownOption = {
|
||||
key: DefaultDirectoryDropdownComponent.lastVisitedKey,
|
||||
text: "Sign in to your last visited directory"
|
||||
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})`
|
||||
text: `${dirc.displayName}(${dirc.tenantId})`,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -35,7 +35,7 @@ export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDi
|
||||
options: dropDownOptions,
|
||||
defaultSelectedKey: this.props.defaultDirectoryId ? this.props.defaultDirectoryId : lastVisitedOption.key,
|
||||
onChange: this._onDropdownChange,
|
||||
className: "defaultDirectoryDropdown"
|
||||
className: "defaultDirectoryDropdown",
|
||||
};
|
||||
|
||||
return <Dropdown {...dropDownProps} />;
|
||||
@@ -56,12 +56,12 @@ export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDi
|
||||
countryCode: undefined,
|
||||
displayName: undefined,
|
||||
domains: [],
|
||||
id: undefined
|
||||
id: undefined,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDirectory = _.find(this.props.directories, d => d.tenantId === option.key);
|
||||
const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === option.key);
|
||||
if (!selectedDirectory) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ const createBlankProps = (): DirectoryListProps => {
|
||||
return {
|
||||
selectedDirectoryId: undefined,
|
||||
directories: [],
|
||||
onNewDirectorySelected: jest.fn()
|
||||
onNewDirectorySelected: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ const createBlankDirectory = (): Tenant => {
|
||||
displayName: undefined,
|
||||
domains: [],
|
||||
id: undefined,
|
||||
tenantId: undefined
|
||||
tenantId: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterText: ""
|
||||
filterText: "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,12 +38,12 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
const filteredItems =
|
||||
originalItems && originalItems.length && filterText
|
||||
? originalItems.filter(
|
||||
directory =>
|
||||
(directory) =>
|
||||
directory.displayName &&
|
||||
directory.displayName.toLowerCase().indexOf(filterText && filterText.toLowerCase()) >= 0
|
||||
)
|
||||
: originalItems;
|
||||
const filteredItemsSelected = filteredItems.map(t => {
|
||||
const filteredItemsSelected = filteredItems.map((t) => {
|
||||
let tenant: ListTenant = t;
|
||||
tenant.selected = t.tenantId === selectedDirectoryId;
|
||||
return tenant;
|
||||
@@ -53,7 +53,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
className: "directoryListFilterTextBox",
|
||||
placeholder: "Filter by directory name",
|
||||
onChange: this._onFilterChanged,
|
||||
ariaLabel: "Directory filter text box"
|
||||
ariaLabel: "Directory filter text box",
|
||||
};
|
||||
|
||||
// TODO: add magnify glass to search bar with onRenderSuffix
|
||||
@@ -69,7 +69,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
|
||||
private _onFilterChanged = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text?: string): void => {
|
||||
this.setState({
|
||||
filterText: text
|
||||
filterText: text,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -84,19 +84,19 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
height: "auto",
|
||||
borderBottom: "1px solid #ccc",
|
||||
padding: "1px 0",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
},
|
||||
rootDisabled: {
|
||||
backgroundColor: "#f1f1f8"
|
||||
backgroundColor: "#f1f1f8",
|
||||
},
|
||||
rootHovered: {
|
||||
backgroundColor: "rgba(85,179,255,.1)"
|
||||
backgroundColor: "rgba(85,179,255,.1)",
|
||||
},
|
||||
flexContainer: {
|
||||
height: "auto",
|
||||
justifyContent: "flex-start"
|
||||
}
|
||||
}
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -115,7 +115,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
|
||||
}
|
||||
const buttonElement = e.currentTarget;
|
||||
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
|
||||
const selectedDirectory = _.find(this.props.directories, d => d.tenantId === selectedDirectoryId);
|
||||
const selectedDirectory = _.find(this.props.directories, (d) => d.tenantId === selectedDirectoryId);
|
||||
|
||||
this.props.onNewDirectorySelected(selectedDirectory);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("Dynamic List Component", () => {
|
||||
placeholder: placeholder,
|
||||
listItems: items,
|
||||
buttonText: mockButton,
|
||||
ariaLabel: mockAriaLabel
|
||||
ariaLabel: mockAriaLabel,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -113,5 +113,5 @@ export class DynamicListViewModel extends WaitsForTemplateViewModel {
|
||||
*/
|
||||
export const DynamicListComponent = {
|
||||
viewModel: DynamicListViewModel,
|
||||
template
|
||||
template,
|
||||
};
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent";
|
||||
import template from "./editor-component.html";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class EditorComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: EditorViewModel,
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
export interface EditorParams extends JsonEditorParams {
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a generic editor component that builds on top of the pre-existing JsonEditorComponent.
|
||||
*/
|
||||
// TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around
|
||||
class EditorViewModel extends JsonEditorViewModel {
|
||||
public params: EditorParams;
|
||||
private static providerRegistered: string[] = [];
|
||||
|
||||
public constructor(params: EditorParams) {
|
||||
super(params);
|
||||
this.params = params;
|
||||
super.createEditor.bind(this);
|
||||
|
||||
/**
|
||||
* setTimeout is needed as creating the edtior manipulates the dom directly and expects
|
||||
* Knockout to have completed all of the initial bindings for the component
|
||||
*/
|
||||
this.params.content() != null &&
|
||||
setTimeout(() => {
|
||||
this.createEditor(this.params.content(), this.configureEditor.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
protected getEditorLanguage(): string {
|
||||
return this.params.contentType;
|
||||
}
|
||||
|
||||
protected registerCompletionItemProvider() {
|
||||
let sqlCompletionItemProvider = new SqlCompletionItemProvider();
|
||||
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
||||
monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider);
|
||||
EditorViewModel.providerRegistered.push("sql");
|
||||
}
|
||||
}
|
||||
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
return ErrorMarkProvider.getErrorMark(input);
|
||||
}
|
||||
}
|
||||
import { JsonEditorParams, JsonEditorViewModel } from "../JsonEditor/JsonEditorComponent";
|
||||
import template from "./editor-component.html";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { SqlCompletionItemProvider, ErrorMarkProvider } from "@azure/cosmos-language-service";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class EditorComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: EditorViewModel,
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
export interface EditorParams extends JsonEditorParams {
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a generic editor component that builds on top of the pre-existing JsonEditorComponent.
|
||||
*/
|
||||
// TODO: Ideally, JsonEditorViewModel should extend EditorViewModel and not the other way around
|
||||
class EditorViewModel extends JsonEditorViewModel {
|
||||
public params: EditorParams;
|
||||
private static providerRegistered: string[] = [];
|
||||
|
||||
public constructor(params: EditorParams) {
|
||||
super(params);
|
||||
this.params = params;
|
||||
super.createEditor.bind(this);
|
||||
|
||||
/**
|
||||
* setTimeout is needed as creating the edtior manipulates the dom directly and expects
|
||||
* Knockout to have completed all of the initial bindings for the component
|
||||
*/
|
||||
this.params.content() != null &&
|
||||
setTimeout(() => {
|
||||
this.createEditor(this.params.content(), this.configureEditor.bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
protected getEditorLanguage(): string {
|
||||
return this.params.contentType;
|
||||
}
|
||||
|
||||
protected registerCompletionItemProvider() {
|
||||
let sqlCompletionItemProvider = new SqlCompletionItemProvider();
|
||||
if (EditorViewModel.providerRegistered.indexOf("sql") < 0) {
|
||||
monaco.languages.registerCompletionItemProvider("sql", sqlCompletionItemProvider);
|
||||
EditorViewModel.providerRegistered.push("sql");
|
||||
}
|
||||
}
|
||||
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
return ErrorMarkProvider.getErrorMark(input);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ export class EditorReact extends React.Component<EditorReactProps> {
|
||||
fontSize: 12,
|
||||
ariaLabel: this.props.ariaLabel,
|
||||
theme: this.props.theme,
|
||||
automaticLayout: true
|
||||
automaticLayout: true,
|
||||
};
|
||||
|
||||
this.rootNode.innerHTML = "";
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import template from "./error-display-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
* This component displays an error as designed in:
|
||||
* https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29
|
||||
* TODO: support "More details"
|
||||
*/
|
||||
export class ErrorDisplayComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: ErrorDisplayViewModel,
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface ErrorDisplayParams {
|
||||
errorMsg: ko.Observable<string>; // Primary message
|
||||
}
|
||||
|
||||
class ErrorDisplayViewModel {
|
||||
public constructor(public params: ErrorDisplayParams) {}
|
||||
}
|
||||
import template from "./error-display-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
* This component displays an error as designed in:
|
||||
* https://microsoft.sharepoint.com/teams/DPX/Modern/DocDB/_layouts/15/WopiFrame.aspx?sourcedoc={66864d4a-f925-4cbe-9eb4-79f8d191a115}&action=edit&wd=target%28DocumentDB%20emulator%2Eone%7CE617D0A7-F77C-4968-B75A-1451049F4FEA%2FError%20notification%7CAA1E4BC9-4D72-472C-B40C-2437FA217226%2F%29
|
||||
* TODO: support "More details"
|
||||
*/
|
||||
export class ErrorDisplayComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: ErrorDisplayViewModel,
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface ErrorDisplayParams {
|
||||
errorMsg: ko.Observable<string>; // Primary message
|
||||
}
|
||||
|
||||
class ErrorDisplayViewModel {
|
||||
public constructor(public params: ErrorDisplayParams) {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="warningErrorContainer" data-bind="visible: !!params.errorMsg()">
|
||||
<div class="warningErrorContent">
|
||||
<span><img src="/error_red.svg" alt="Error"/></span>
|
||||
<span class="settingErrorMsg warningErrorDetailsLinkContainer" data-bind="text: params.errorMsg()"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warningErrorContainer" data-bind="visible: !!params.errorMsg()">
|
||||
<div class="warningErrorContent">
|
||||
<span><img src="/error_red.svg" alt="Error" /></span>
|
||||
<span class="settingErrorMsg warningErrorDetailsLinkContainer" data-bind="text: params.errorMsg()"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,23 +15,23 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
const baseUrlOptions = [
|
||||
{ key: "https://localhost:1234/explorer.html", text: "localhost:1234" },
|
||||
{ key: "https://cosmos.azure.com/explorer.html", text: "cosmos.azure.com" },
|
||||
{ key: "https://portal.azure.com", text: "portal" }
|
||||
{ key: "https://portal.azure.com", text: "portal" },
|
||||
];
|
||||
|
||||
const platformOptions = [
|
||||
{ key: "Hosted", text: "Hosted" },
|
||||
{ key: "Portal", text: "Portal" },
|
||||
{ key: "Emulator", text: "Emulator" },
|
||||
{ key: "", text: "None" }
|
||||
{ key: "", text: "None" },
|
||||
];
|
||||
|
||||
// React hooks to keep state
|
||||
const [baseUrl, setBaseUrl] = React.useState<IDropdownOption>(
|
||||
baseUrlOptions.find(o => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0]
|
||||
baseUrlOptions.find((o) => o.key === window.location.origin + window.location.pathname) || baseUrlOptions[0]
|
||||
);
|
||||
const [platform, setPlatform] = React.useState<IDropdownOption>(
|
||||
urlParams.has("platform")
|
||||
? platformOptions.find(o => o.key === urlParams.get("platform")) || platformOptions[0]
|
||||
? platformOptions.find((o) => o.key === urlParams.get("platform")) || platformOptions[0]
|
||||
: platformOptions[0]
|
||||
);
|
||||
|
||||
@@ -52,13 +52,13 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
{
|
||||
key: "feature.enableLinkInjection",
|
||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||
value: "true"
|
||||
value: "true",
|
||||
},
|
||||
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
|
||||
{
|
||||
key: "feature.enablefixedcollectionwithsharedthroughput",
|
||||
label: "Enable fixed collection with shared throughput",
|
||||
value: "true"
|
||||
value: "true",
|
||||
},
|
||||
{ key: "feature.ttl90days", label: "TTL 90 days", value: "true" },
|
||||
{ key: "feature.enablenotebooks", label: "Enable notebooks", value: "true" },
|
||||
@@ -66,10 +66,10 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
key: "feature.customportal",
|
||||
label: "Force Production portal (portal only)",
|
||||
value: "false",
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com",
|
||||
},
|
||||
{ key: "feature.enablespark", label: "Enable Synapse", value: "true" },
|
||||
{ key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" }
|
||||
{ key: "feature.enableautopilotv2", label: "Enable Auto-pilot V2", value: "true" },
|
||||
];
|
||||
|
||||
const stringFeatures: {
|
||||
@@ -88,23 +88,23 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
key: "dataExplorerSource",
|
||||
label: "Data Explorer Source (portal only)",
|
||||
placeholder: "https://localhost:1234/explorer.html",
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com"
|
||||
disabled: (): boolean => baseUrl.key !== "https://portal.azure.com",
|
||||
},
|
||||
{ key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" }
|
||||
{ key: "feature.livyendpoint", label: "Livy endpoint", placeholder: "" },
|
||||
];
|
||||
|
||||
booleanFeatures.forEach(
|
||||
f => (f.reactState = React.useState<boolean>(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false))
|
||||
(f) => (f.reactState = React.useState<boolean>(urlParams.has(f.key) ? urlParams.get(f.key) === "true" : false))
|
||||
);
|
||||
stringFeatures.forEach(
|
||||
f => (f.reactState = React.useState<string>(urlParams.has(f.key) ? urlParams.get(f.key) : undefined))
|
||||
(f) => (f.reactState = React.useState<string>(urlParams.has(f.key) ? urlParams.get(f.key) : undefined))
|
||||
);
|
||||
|
||||
const buildUrl = (): string => {
|
||||
const fragments = (platform.key === "" ? [] : [`platform=${platform.key}`])
|
||||
.concat(booleanFeatures.map(f => (f.reactState[0] ? `${f.key}=${f.value}` : "")))
|
||||
.concat(stringFeatures.map(f => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : "")))
|
||||
.filter(v => v && v.length > 0);
|
||||
.concat(booleanFeatures.map((f) => (f.reactState[0] ? `${f.key}=${f.value}` : "")))
|
||||
.concat(stringFeatures.map((f) => (f.reactState[0] ? `${f.key}=${encodeURIComponent(f.reactState[0])}` : "")))
|
||||
.filter((v) => v && v.length > 0);
|
||||
|
||||
const paramString = fragments.length < 1 ? "" : `?${fragments.join("&")}`;
|
||||
return `${baseUrl.key}${paramString}`;
|
||||
@@ -119,38 +119,38 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
};
|
||||
|
||||
booleanFeatures.forEach(
|
||||
f =>
|
||||
(f) =>
|
||||
(f.onChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean): void => {
|
||||
f.reactState[1](checked);
|
||||
})
|
||||
);
|
||||
|
||||
stringFeatures.forEach(
|
||||
f =>
|
||||
(f) =>
|
||||
(f.onChange = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string): void => {
|
||||
f.reactState[1](newValue);
|
||||
})
|
||||
);
|
||||
|
||||
const onNotebookShortcut = (): void => {
|
||||
booleanFeatures.find(f => f.key === "feature.enablenotebooks").reactState[1](true);
|
||||
booleanFeatures.find((f) => f.key === "feature.enablenotebooks").reactState[1](true);
|
||||
stringFeatures
|
||||
.find(f => f.key === "feature.notebookserverurl")
|
||||
.find((f) => f.key === "feature.notebookserverurl")
|
||||
.reactState[1]("https://localhost:10001/12345/notebook/");
|
||||
stringFeatures.find(f => f.key === "feature.notebookservertoken").reactState[1]("token");
|
||||
stringFeatures.find(f => f.key === "feature.notebookbasepath").reactState[1]("./notebooks");
|
||||
setPlatform(platformOptions.find(o => o.key === "Hosted"));
|
||||
stringFeatures.find((f) => f.key === "feature.notebookservertoken").reactState[1]("token");
|
||||
stringFeatures.find((f) => f.key === "feature.notebookbasepath").reactState[1]("./notebooks");
|
||||
setPlatform(platformOptions.find((o) => o.key === "Hosted"));
|
||||
};
|
||||
|
||||
const onPortalLocalDEShortcut = (): void => {
|
||||
setBaseUrl(baseUrlOptions.find(o => o.key === "https://portal.azure.com"));
|
||||
setPlatform(platformOptions.find(o => o.key === "Portal"));
|
||||
stringFeatures.find(f => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html");
|
||||
setBaseUrl(baseUrlOptions.find((o) => o.key === "https://portal.azure.com"));
|
||||
setPlatform(platformOptions.find((o) => o.key === "Portal"));
|
||||
stringFeatures.find((f) => f.key === "dataExplorerSource").reactState[1]("https://localhost:1234/explorer.html");
|
||||
};
|
||||
|
||||
const onReset = (): void => {
|
||||
booleanFeatures.forEach(f => f.reactState[1](false));
|
||||
stringFeatures.forEach(f => f.reactState[1](""));
|
||||
booleanFeatures.forEach((f) => f.reactState[1](false));
|
||||
stringFeatures.forEach((f) => f.reactState[1](""));
|
||||
};
|
||||
|
||||
const stackTokens = { childrenGap: 10 };
|
||||
@@ -169,7 +169,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
const anchorOptions = {
|
||||
href: buildUrl(),
|
||||
target: "_blank",
|
||||
rel: "noopener"
|
||||
rel: "noopener",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -201,7 +201,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
</Stack>
|
||||
<Stack horizontal>
|
||||
<Stack className="checkboxRow" horizontalAlign="space-between">
|
||||
{leftBooleanFeatures.map(f => (
|
||||
{leftBooleanFeatures.map((f) => (
|
||||
<Checkbox
|
||||
key={f.key}
|
||||
label={f.label}
|
||||
@@ -212,7 +212,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
))}
|
||||
</Stack>
|
||||
<Stack className="checkboxRow" horizontalAlign="space-between">
|
||||
{rightBooleanFeatures.map(f => (
|
||||
{rightBooleanFeatures.map((f) => (
|
||||
<Checkbox
|
||||
key={f.key}
|
||||
label={f.label}
|
||||
@@ -225,7 +225,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
</Stack>
|
||||
<Stack horizontal tokens={stackTokens}>
|
||||
<Stack horizontalAlign="space-between">
|
||||
{leftStringFeatures.map(f => (
|
||||
{leftStringFeatures.map((f) => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
value={f.reactState[0]}
|
||||
@@ -238,7 +238,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||
))}
|
||||
</Stack>
|
||||
<Stack horizontalAlign="space-between">
|
||||
{rightStringFeatures.map(f => (
|
||||
{rightStringFeatures.map((f) => (
|
||||
<TextField
|
||||
key={f.key}
|
||||
value={f.reactState[0]}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element =>
|
||||
container: {
|
||||
display: "flex",
|
||||
flexFlow: "column nowrap",
|
||||
alignItems: "stretch"
|
||||
alignItems: "stretch",
|
||||
},
|
||||
header: [
|
||||
// tslint:disable-next-line:deprecation
|
||||
@@ -32,16 +32,16 @@ export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element =>
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
fontWeight: FontWeights.semibold,
|
||||
padding: "12px 12px 14px 24px"
|
||||
}
|
||||
padding: "12px 12px 14px 24px",
|
||||
},
|
||||
],
|
||||
body: {
|
||||
flex: "4 4 auto",
|
||||
overflowY: "hidden",
|
||||
marginBottom: 40,
|
||||
height: "100%",
|
||||
display: "flex"
|
||||
}
|
||||
display: "flex",
|
||||
},
|
||||
});
|
||||
|
||||
const iconButtonStyles = {
|
||||
@@ -49,11 +49,11 @@ export const FeaturePanelLauncher: React.FunctionComponent = (): JSX.Element =>
|
||||
color: theme.palette.neutralPrimary,
|
||||
marginLeft: "auto",
|
||||
marginTop: "4px",
|
||||
marginRight: "2px"
|
||||
marginRight: "2px",
|
||||
},
|
||||
rootHovered: {
|
||||
color: theme.palette.neutralDark
|
||||
}
|
||||
color: theme.palette.neutralDark,
|
||||
},
|
||||
};
|
||||
const cancelIcon: IIconProps = { iconName: "Cancel" };
|
||||
const hideModal = (): void => showModal(false);
|
||||
|
||||
@@ -1,132 +1,132 @@
|
||||
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import UrlUtility from "../../../Common/UrlUtility";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface AddRepoComponentProps {
|
||||
container: Explorer;
|
||||
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
|
||||
pinRepo: (item: RepoListItem) => void;
|
||||
}
|
||||
|
||||
interface AddRepoComponentState {
|
||||
textFieldValue: string;
|
||||
textFieldErrorMessage: string;
|
||||
}
|
||||
|
||||
export class AddRepoComponent extends React.Component<AddRepoComponentProps, AddRepoComponentState> {
|
||||
private static readonly DescriptionText =
|
||||
"Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: ";
|
||||
private static readonly ButtonText = "Add";
|
||||
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
||||
private static readonly TextFieldErrorMessage = "Invalid url";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
|
||||
constructor(props: AddRepoComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
textFieldValue: "",
|
||||
textFieldErrorMessage: undefined
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const textFieldProps: ITextFieldProps = {
|
||||
placeholder: AddRepoComponent.TextFieldPlaceholder,
|
||||
autoFocus: true,
|
||||
value: this.state.textFieldValue,
|
||||
errorMessage: this.state.textFieldErrorMessage,
|
||||
onChange: this.onTextFieldChange
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: AddRepoComponent.ButtonText,
|
||||
ariaLabel: AddRepoComponent.ButtonText,
|
||||
onClick: this.onAddRepoButtonClick
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p style={{ marginBottom: ChildrenMargin }}>{AddRepoComponent.DescriptionText}</p>
|
||||
<TextField {...textFieldProps} />
|
||||
<DefaultButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onTextFieldChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
this.setState({
|
||||
textFieldValue: newValue || "",
|
||||
textFieldErrorMessage: undefined
|
||||
});
|
||||
};
|
||||
|
||||
private onAddRepoButtonClick = async (): Promise<void> => {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, {
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
});
|
||||
let enteredUrl = this.state.textFieldValue;
|
||||
if (enteredUrl.indexOf("/tree/") === -1) {
|
||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
|
||||
}
|
||||
|
||||
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
||||
if (repoInfo) {
|
||||
this.setState({
|
||||
textFieldValue: "",
|
||||
textFieldErrorMessage: undefined
|
||||
});
|
||||
|
||||
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
|
||||
if (repo) {
|
||||
const item: RepoListItem = {
|
||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
||||
repo,
|
||||
branches: [
|
||||
{
|
||||
name: repoInfo.branch
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.NotebooksGitHubManualRepoAdd,
|
||||
{
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return this.props.pinRepo(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage
|
||||
});
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.NotebooksGitHubManualRepoAdd,
|
||||
{
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
error: AddRepoComponent.TextFieldErrorMessage
|
||||
},
|
||||
startKey
|
||||
);
|
||||
};
|
||||
}
|
||||
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
import UrlUtility from "../../../Common/UrlUtility";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface AddRepoComponentProps {
|
||||
container: Explorer;
|
||||
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
|
||||
pinRepo: (item: RepoListItem) => void;
|
||||
}
|
||||
|
||||
interface AddRepoComponentState {
|
||||
textFieldValue: string;
|
||||
textFieldErrorMessage: string;
|
||||
}
|
||||
|
||||
export class AddRepoComponent extends React.Component<AddRepoComponentProps, AddRepoComponentState> {
|
||||
private static readonly DescriptionText =
|
||||
"Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: ";
|
||||
private static readonly ButtonText = "Add";
|
||||
private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch";
|
||||
private static readonly TextFieldErrorMessage = "Invalid url";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
|
||||
constructor(props: AddRepoComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
textFieldValue: "",
|
||||
textFieldErrorMessage: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const textFieldProps: ITextFieldProps = {
|
||||
placeholder: AddRepoComponent.TextFieldPlaceholder,
|
||||
autoFocus: true,
|
||||
value: this.state.textFieldValue,
|
||||
errorMessage: this.state.textFieldErrorMessage,
|
||||
onChange: this.onTextFieldChange,
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: AddRepoComponent.ButtonText,
|
||||
ariaLabel: AddRepoComponent.ButtonText,
|
||||
onClick: this.onAddRepoButtonClick,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p style={{ marginBottom: ChildrenMargin }}>{AddRepoComponent.DescriptionText}</p>
|
||||
<TextField {...textFieldProps} />
|
||||
<DefaultButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onTextFieldChange = (
|
||||
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
newValue?: string
|
||||
): void => {
|
||||
this.setState({
|
||||
textFieldValue: newValue || "",
|
||||
textFieldErrorMessage: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private onAddRepoButtonClick = async (): Promise<void> => {
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, {
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
});
|
||||
let enteredUrl = this.state.textFieldValue;
|
||||
if (enteredUrl.indexOf("/tree/") === -1) {
|
||||
enteredUrl = UrlUtility.createUri(enteredUrl, `tree/${AddRepoComponent.DefaultBranchName}`);
|
||||
}
|
||||
|
||||
const repoInfo = GitHubUtils.fromRepoUri(enteredUrl);
|
||||
if (repoInfo) {
|
||||
this.setState({
|
||||
textFieldValue: "",
|
||||
textFieldErrorMessage: undefined,
|
||||
});
|
||||
|
||||
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
|
||||
if (repo) {
|
||||
const item: RepoListItem = {
|
||||
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
|
||||
repo,
|
||||
branches: [
|
||||
{
|
||||
name: repoInfo.branch,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.NotebooksGitHubManualRepoAdd,
|
||||
{
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
return this.props.pinRepo(item);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage,
|
||||
});
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.NotebooksGitHubManualRepoAdd,
|
||||
{
|
||||
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
|
||||
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Notebook,
|
||||
error: AddRepoComponent.TextFieldErrorMessage,
|
||||
},
|
||||
startKey
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
import {
|
||||
ChoiceGroup,
|
||||
IButtonProps,
|
||||
IChoiceGroupProps,
|
||||
PrimaryButton,
|
||||
IChoiceGroupOption
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
|
||||
export interface AuthorizeAccessComponentProps {
|
||||
scope: string;
|
||||
authorizeAccess: (scope: string) => void;
|
||||
}
|
||||
|
||||
export interface AuthorizeAccessComponentState {
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export class AuthorizeAccessComponent extends React.Component<
|
||||
AuthorizeAccessComponentProps,
|
||||
AuthorizeAccessComponentState
|
||||
> {
|
||||
// Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos.
|
||||
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
|
||||
public static readonly Scopes = {
|
||||
Public: {
|
||||
key: "public_repo",
|
||||
text: "Public repos only"
|
||||
},
|
||||
PublicAndPrivate: {
|
||||
key: "repo",
|
||||
text: "Public and private repos"
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly DescriptionPara1 =
|
||||
"Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer.";
|
||||
private static readonly DescriptionPara2 =
|
||||
"Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: ";
|
||||
private static readonly AuthorizeButtonText = "Authorize access";
|
||||
|
||||
private onChoiceGroupChange = (event: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void =>
|
||||
this.setState({
|
||||
scope: option.key
|
||||
});
|
||||
|
||||
private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope);
|
||||
|
||||
constructor(props: AuthorizeAccessComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scope: this.props.scope
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const choiceGroupProps: IChoiceGroupProps = {
|
||||
options: [
|
||||
{
|
||||
key: AuthorizeAccessComponent.Scopes.Public.key,
|
||||
text: AuthorizeAccessComponent.Scopes.Public.text,
|
||||
ariaLabel: AuthorizeAccessComponent.Scopes.Public.text
|
||||
},
|
||||
{
|
||||
key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key,
|
||||
text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text,
|
||||
ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text
|
||||
}
|
||||
],
|
||||
selectedKey: this.state.scope,
|
||||
onChange: this.onChoiceGroupChange
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: AuthorizeAccessComponent.AuthorizeButtonText,
|
||||
ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText,
|
||||
onClick: this.onButtonClick
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{AuthorizeAccessComponent.DescriptionPara1}</p>
|
||||
<p style={{ marginTop: ChildrenMargin }}>{AuthorizeAccessComponent.DescriptionPara2}</p>
|
||||
<ChoiceGroup style={{ marginTop: ChildrenMargin }} {...choiceGroupProps} />
|
||||
<PrimaryButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChoiceGroup,
|
||||
IButtonProps,
|
||||
IChoiceGroupProps,
|
||||
PrimaryButton,
|
||||
IChoiceGroupOption,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { ChildrenMargin } from "./GitHubStyleConstants";
|
||||
|
||||
export interface AuthorizeAccessComponentProps {
|
||||
scope: string;
|
||||
authorizeAccess: (scope: string) => void;
|
||||
}
|
||||
|
||||
export interface AuthorizeAccessComponentState {
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export class AuthorizeAccessComponent extends React.Component<
|
||||
AuthorizeAccessComponentProps,
|
||||
AuthorizeAccessComponentState
|
||||
> {
|
||||
// Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos.
|
||||
// https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/
|
||||
public static readonly Scopes = {
|
||||
Public: {
|
||||
key: "public_repo",
|
||||
text: "Public repos only",
|
||||
},
|
||||
PublicAndPrivate: {
|
||||
key: "repo",
|
||||
text: "Public and private repos",
|
||||
},
|
||||
};
|
||||
|
||||
private static readonly DescriptionPara1 =
|
||||
"Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer.";
|
||||
private static readonly DescriptionPara2 =
|
||||
"Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: ";
|
||||
private static readonly AuthorizeButtonText = "Authorize access";
|
||||
|
||||
private onChoiceGroupChange = (event: React.SyntheticEvent<HTMLElement>, option: IChoiceGroupOption): void =>
|
||||
this.setState({
|
||||
scope: option.key,
|
||||
});
|
||||
|
||||
private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope);
|
||||
|
||||
constructor(props: AuthorizeAccessComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
scope: this.props.scope,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const choiceGroupProps: IChoiceGroupProps = {
|
||||
options: [
|
||||
{
|
||||
key: AuthorizeAccessComponent.Scopes.Public.key,
|
||||
text: AuthorizeAccessComponent.Scopes.Public.text,
|
||||
ariaLabel: AuthorizeAccessComponent.Scopes.Public.text,
|
||||
},
|
||||
{
|
||||
key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key,
|
||||
text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text,
|
||||
ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text,
|
||||
},
|
||||
],
|
||||
selectedKey: this.state.scope,
|
||||
onChange: this.onChoiceGroupChange,
|
||||
};
|
||||
|
||||
const buttonProps: IButtonProps = {
|
||||
text: AuthorizeAccessComponent.AuthorizeButtonText,
|
||||
ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText,
|
||||
onClick: this.onButtonClick,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{AuthorizeAccessComponent.DescriptionPara1}</p>
|
||||
<p style={{ marginTop: ChildrenMargin }}>{AuthorizeAccessComponent.DescriptionPara2}</p>
|
||||
<ChoiceGroup style={{ marginTop: ChildrenMargin }} {...choiceGroupProps} />
|
||||
<PrimaryButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent";
|
||||
import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent";
|
||||
import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants";
|
||||
import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent";
|
||||
|
||||
export interface GitHubReposComponentProps {
|
||||
showAuthorizeAccess: boolean;
|
||||
authorizeAccessProps: AuthorizeAccessComponentProps;
|
||||
reposListProps: ReposListComponentProps;
|
||||
addRepoProps: AddRepoComponentProps;
|
||||
resetConnection: () => void;
|
||||
onOkClick: () => void;
|
||||
onCancelClick: () => void;
|
||||
}
|
||||
|
||||
export interface RepoListItem {
|
||||
key: string;
|
||||
repo: IGitHubRepo;
|
||||
branches: IGitHubBranch[];
|
||||
}
|
||||
|
||||
export class GitHubReposComponent extends React.Component<GitHubReposComponentProps> {
|
||||
public static readonly ConnectToGitHubTitle = "Connect to GitHub";
|
||||
public static readonly ManageGitHubRepoTitle = "Manage GitHub settings";
|
||||
private static readonly ManageGitHubRepoDescription =
|
||||
"Select your GitHub repos and branch(es) to pin to your notebooks workspace.";
|
||||
private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings.";
|
||||
private static readonly OKButtonText = "OK";
|
||||
private static readonly CancelButtonText = "Cancel";
|
||||
|
||||
public render(): JSX.Element {
|
||||
const header: JSX.Element = (
|
||||
<p>
|
||||
{this.props.showAuthorizeAccess
|
||||
? GitHubReposComponent.ConnectToGitHubTitle
|
||||
: GitHubReposComponent.ManageGitHubRepoTitle}
|
||||
</p>
|
||||
);
|
||||
|
||||
const content: JSX.Element = this.props.showAuthorizeAccess ? (
|
||||
<AuthorizeAccessComponent {...this.props.authorizeAccessProps} />
|
||||
) : (
|
||||
<>
|
||||
<p>{GitHubReposComponent.ManageGitHubRepoDescription}</p>
|
||||
<Link style={{ marginTop: ChildrenMargin }} onClick={this.props.resetConnection}>
|
||||
{GitHubReposComponent.ManageGitHubRepoResetConnection}
|
||||
</Link>
|
||||
<ReposListComponent {...this.props.reposListProps} />
|
||||
</>
|
||||
);
|
||||
|
||||
const okProps: IButtonProps = {
|
||||
text: GitHubReposComponent.OKButtonText,
|
||||
ariaLabel: GitHubReposComponent.OKButtonText,
|
||||
onClick: this.props.onOkClick
|
||||
};
|
||||
|
||||
const cancelProps: IButtonProps = {
|
||||
text: GitHubReposComponent.CancelButtonText,
|
||||
ariaLabel: GitHubReposComponent.CancelButtonText,
|
||||
onClick: this.props.onCancelClick
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"firstdivbg headerline"} role="heading" aria-level={2}>
|
||||
{header}
|
||||
</div>
|
||||
<div className={"paneMainContent"}>{content}</div>
|
||||
{!this.props.showAuthorizeAccess && (
|
||||
<>
|
||||
<div className={"paneFooter"} style={ContentFooterStyle}>
|
||||
<AddRepoComponent {...this.props.addRepoProps} />
|
||||
</div>
|
||||
<div className={"paneFooter"} style={ButtonsFooterStyle}>
|
||||
<PrimaryButton {...okProps} />
|
||||
<DefaultButton style={{ marginLeft: ChildrenMargin }} {...cancelProps} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient";
|
||||
import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent";
|
||||
import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent";
|
||||
import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants";
|
||||
import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent";
|
||||
|
||||
export interface GitHubReposComponentProps {
|
||||
showAuthorizeAccess: boolean;
|
||||
authorizeAccessProps: AuthorizeAccessComponentProps;
|
||||
reposListProps: ReposListComponentProps;
|
||||
addRepoProps: AddRepoComponentProps;
|
||||
resetConnection: () => void;
|
||||
onOkClick: () => void;
|
||||
onCancelClick: () => void;
|
||||
}
|
||||
|
||||
export interface RepoListItem {
|
||||
key: string;
|
||||
repo: IGitHubRepo;
|
||||
branches: IGitHubBranch[];
|
||||
}
|
||||
|
||||
export class GitHubReposComponent extends React.Component<GitHubReposComponentProps> {
|
||||
public static readonly ConnectToGitHubTitle = "Connect to GitHub";
|
||||
public static readonly ManageGitHubRepoTitle = "Manage GitHub settings";
|
||||
private static readonly ManageGitHubRepoDescription =
|
||||
"Select your GitHub repos and branch(es) to pin to your notebooks workspace.";
|
||||
private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings.";
|
||||
private static readonly OKButtonText = "OK";
|
||||
private static readonly CancelButtonText = "Cancel";
|
||||
|
||||
public render(): JSX.Element {
|
||||
const header: JSX.Element = (
|
||||
<p>
|
||||
{this.props.showAuthorizeAccess
|
||||
? GitHubReposComponent.ConnectToGitHubTitle
|
||||
: GitHubReposComponent.ManageGitHubRepoTitle}
|
||||
</p>
|
||||
);
|
||||
|
||||
const content: JSX.Element = this.props.showAuthorizeAccess ? (
|
||||
<AuthorizeAccessComponent {...this.props.authorizeAccessProps} />
|
||||
) : (
|
||||
<>
|
||||
<p>{GitHubReposComponent.ManageGitHubRepoDescription}</p>
|
||||
<Link style={{ marginTop: ChildrenMargin }} onClick={this.props.resetConnection}>
|
||||
{GitHubReposComponent.ManageGitHubRepoResetConnection}
|
||||
</Link>
|
||||
<ReposListComponent {...this.props.reposListProps} />
|
||||
</>
|
||||
);
|
||||
|
||||
const okProps: IButtonProps = {
|
||||
text: GitHubReposComponent.OKButtonText,
|
||||
ariaLabel: GitHubReposComponent.OKButtonText,
|
||||
onClick: this.props.onOkClick,
|
||||
};
|
||||
|
||||
const cancelProps: IButtonProps = {
|
||||
text: GitHubReposComponent.CancelButtonText,
|
||||
ariaLabel: GitHubReposComponent.CancelButtonText,
|
||||
onClick: this.props.onCancelClick,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"firstdivbg headerline"} role="heading" aria-level={2}>
|
||||
{header}
|
||||
</div>
|
||||
<div className={"paneMainContent"}>{content}</div>
|
||||
{!this.props.showAuthorizeAccess && (
|
||||
<>
|
||||
<div className={"paneFooter"} style={ContentFooterStyle}>
|
||||
<AddRepoComponent {...this.props.addRepoProps} />
|
||||
</div>
|
||||
<div className={"paneFooter"} style={ButtonsFooterStyle}>
|
||||
<PrimaryButton {...okProps} />
|
||||
<DefaultButton style={{ marginLeft: ChildrenMargin }} {...cancelProps} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
|
||||
|
||||
export class GitHubReposComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GitHubReposComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GitHubReposComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
|
||||
|
||||
export class GitHubReposComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GitHubReposComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GitHubReposComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import {
|
||||
IStyleFunctionOrObject,
|
||||
ICheckboxStyleProps,
|
||||
ICheckboxStyles,
|
||||
IDropdownStyles,
|
||||
IDropdownStyleProps
|
||||
} from "office-ui-fabric-react";
|
||||
|
||||
export const ButtonsFooterStyle: React.CSSProperties = {
|
||||
padding: 14,
|
||||
height: "auto"
|
||||
};
|
||||
|
||||
export const ContentFooterStyle: React.CSSProperties = {
|
||||
padding: "10px 24px 10px 24px",
|
||||
height: "auto"
|
||||
};
|
||||
|
||||
export const ChildrenMargin = 10;
|
||||
export const FontSize = 12;
|
||||
|
||||
export const ReposListCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0"
|
||||
},
|
||||
text: {
|
||||
fontSize: FontSize
|
||||
}
|
||||
};
|
||||
|
||||
export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: FontSize
|
||||
},
|
||||
root: {
|
||||
padding: 0
|
||||
},
|
||||
text: {
|
||||
fontSize: FontSize
|
||||
}
|
||||
};
|
||||
|
||||
export const BranchesDropdownStyles: IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> = {
|
||||
title: {
|
||||
fontSize: FontSize
|
||||
}
|
||||
};
|
||||
|
||||
export const BranchesDropdownOptionContainerStyle: React.CSSProperties = {
|
||||
padding: 8
|
||||
};
|
||||
|
||||
export const ReposListRepoColumnMinWidth = 192;
|
||||
export const ReposListBranchesColumnWidth = 116;
|
||||
export const BranchesDropdownWidth = 200;
|
||||
import {
|
||||
IStyleFunctionOrObject,
|
||||
ICheckboxStyleProps,
|
||||
ICheckboxStyles,
|
||||
IDropdownStyles,
|
||||
IDropdownStyleProps,
|
||||
} from "office-ui-fabric-react";
|
||||
|
||||
export const ButtonsFooterStyle: React.CSSProperties = {
|
||||
padding: 14,
|
||||
height: "auto",
|
||||
};
|
||||
|
||||
export const ContentFooterStyle: React.CSSProperties = {
|
||||
padding: "10px 24px 10px 24px",
|
||||
height: "auto",
|
||||
};
|
||||
|
||||
export const ChildrenMargin = 10;
|
||||
export const FontSize = 12;
|
||||
|
||||
export const ReposListCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0",
|
||||
},
|
||||
text: {
|
||||
fontSize: FontSize,
|
||||
},
|
||||
};
|
||||
|
||||
export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: FontSize,
|
||||
},
|
||||
root: {
|
||||
padding: 0,
|
||||
},
|
||||
text: {
|
||||
fontSize: FontSize,
|
||||
},
|
||||
};
|
||||
|
||||
export const BranchesDropdownStyles: IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> = {
|
||||
title: {
|
||||
fontSize: FontSize,
|
||||
},
|
||||
};
|
||||
|
||||
export const BranchesDropdownOptionContainerStyle: React.CSSProperties = {
|
||||
padding: 8,
|
||||
};
|
||||
|
||||
export const ReposListRepoColumnMinWidth = 192;
|
||||
export const ReposListBranchesColumnWidth = 116;
|
||||
export const BranchesDropdownWidth = 200;
|
||||
|
||||
@@ -1,304 +1,304 @@
|
||||
import {
|
||||
Checkbox,
|
||||
DetailsList,
|
||||
DetailsRow,
|
||||
Dropdown,
|
||||
ICheckboxProps,
|
||||
IDetailsFooterProps,
|
||||
IDetailsListProps,
|
||||
IDetailsRowBaseProps,
|
||||
IDropdown,
|
||||
IDropdownOption,
|
||||
IDropdownProps,
|
||||
ILinkProps,
|
||||
ISelectableDroppableTextProps,
|
||||
Link,
|
||||
ResponsiveMode,
|
||||
SelectionMode,
|
||||
Text
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import {
|
||||
BranchesDropdownCheckboxStyles,
|
||||
BranchesDropdownOptionContainerStyle,
|
||||
ReposListCheckboxStyles,
|
||||
ReposListRepoColumnMinWidth,
|
||||
ReposListBranchesColumnWidth,
|
||||
BranchesDropdownWidth,
|
||||
BranchesDropdownStyles
|
||||
} from "./GitHubStyleConstants";
|
||||
|
||||
export interface ReposListComponentProps {
|
||||
branchesProps: Record<string, BranchesProps>; // key'd by repo key
|
||||
pinnedReposProps: PinnedReposProps;
|
||||
unpinnedReposProps: UnpinnedReposProps;
|
||||
pinRepo: (repo: RepoListItem) => void;
|
||||
unpinRepo: (repo: RepoListItem) => void;
|
||||
}
|
||||
|
||||
export interface BranchesProps {
|
||||
branches: IGitHubBranch[];
|
||||
lastPageInfo?: IGitHubPageInfo;
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
export interface PinnedReposProps {
|
||||
repos: RepoListItem[];
|
||||
}
|
||||
|
||||
export interface UnpinnedReposProps {
|
||||
repos: RepoListItem[];
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
export class ReposListComponent extends React.Component<ReposListComponentProps> {
|
||||
private static readonly PinnedReposColumnName = "Pinned repos";
|
||||
private static readonly UnpinnedReposColumnName = "Unpinned repos";
|
||||
private static readonly BranchesColumnName = "Branches";
|
||||
private static readonly LoadingText = "Loading...";
|
||||
private static readonly LoadMoreText = "Load more";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
private static readonly FooterIndex = -1;
|
||||
|
||||
public render(): JSX.Element {
|
||||
const pinnedReposListProps: IDetailsListProps = {
|
||||
styles: {
|
||||
contentWrapper: {
|
||||
height: this.props.pinnedReposProps.repos.length ? undefined : 0
|
||||
}
|
||||
},
|
||||
items: this.props.pinnedReposProps.repos,
|
||||
getKey: ReposListComponent.getKey,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
columns: [
|
||||
{
|
||||
key: ReposListComponent.PinnedReposColumnName,
|
||||
name: ReposListComponent.PinnedReposColumnName,
|
||||
ariaLabel: ReposListComponent.PinnedReposColumnName,
|
||||
minWidth: ReposListRepoColumnMinWidth,
|
||||
onRender: this.onRenderPinnedReposColumnItem
|
||||
},
|
||||
{
|
||||
key: ReposListComponent.BranchesColumnName,
|
||||
name: ReposListComponent.BranchesColumnName,
|
||||
ariaLabel: ReposListComponent.BranchesColumnName,
|
||||
minWidth: ReposListBranchesColumnWidth,
|
||||
maxWidth: ReposListBranchesColumnWidth,
|
||||
onRender: this.onRenderPinnedReposBranchesColumnItem
|
||||
}
|
||||
],
|
||||
onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter
|
||||
};
|
||||
|
||||
const unpinnedReposListProps: IDetailsListProps = {
|
||||
items: this.props.unpinnedReposProps.repos,
|
||||
getKey: ReposListComponent.getKey,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
columns: [
|
||||
{
|
||||
key: ReposListComponent.UnpinnedReposColumnName,
|
||||
name: ReposListComponent.UnpinnedReposColumnName,
|
||||
ariaLabel: ReposListComponent.UnpinnedReposColumnName,
|
||||
minWidth: ReposListRepoColumnMinWidth,
|
||||
onRender: this.onRenderUnpinnedReposColumnItem
|
||||
},
|
||||
{
|
||||
key: ReposListComponent.BranchesColumnName,
|
||||
name: ReposListComponent.BranchesColumnName,
|
||||
ariaLabel: ReposListComponent.BranchesColumnName,
|
||||
minWidth: ReposListBranchesColumnWidth,
|
||||
maxWidth: ReposListBranchesColumnWidth,
|
||||
onRender: this.onRenderUnpinnedReposBranchesColumnItem
|
||||
}
|
||||
],
|
||||
onRenderDetailsFooter:
|
||||
this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore
|
||||
? this.onRenderReposFooter
|
||||
: undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsList {...pinnedReposListProps} />
|
||||
<DetailsList {...unpinnedReposListProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <Text>None</Text>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
||||
styles: ReposListCheckboxStyles,
|
||||
defaultChecked: true,
|
||||
onChange: () => this.props.unpinRepo(item)
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
};
|
||||
|
||||
private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
||||
const options: IDropdownOption[] = branchesProps.branches.map(branch => ({
|
||||
key: branch.name,
|
||||
text: branch.name,
|
||||
data: item,
|
||||
disabled: item.branches.length === 1 && branch.name === item.branches[0].name,
|
||||
selected: item.branches.findIndex(element => element.name === branch.name) !== -1
|
||||
}));
|
||||
|
||||
if (branchesProps.hasMore || branchesProps.isLoading) {
|
||||
const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText;
|
||||
options.push({
|
||||
key: text,
|
||||
text,
|
||||
data: item,
|
||||
index: ReposListComponent.FooterIndex
|
||||
});
|
||||
}
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
styles: BranchesDropdownStyles,
|
||||
dropdownWidth: BranchesDropdownWidth,
|
||||
responsiveMode: ResponsiveMode.large,
|
||||
options,
|
||||
onRenderList: this.onRenderBranchesDropdownList
|
||||
};
|
||||
|
||||
if (item.branches.length === 1) {
|
||||
dropdownProps.placeholder = item.branches[0].name;
|
||||
} else if (item.branches.length > 1) {
|
||||
dropdownProps.placeholder = `${item.branches.length} branches`;
|
||||
}
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
};
|
||||
|
||||
private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
styles: BranchesDropdownStyles,
|
||||
options: [],
|
||||
placeholder: ReposListComponent.DefaultBranchName,
|
||||
disabled: true
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
};
|
||||
|
||||
private onRenderBranchesDropdownList = (
|
||||
props: ISelectableDroppableTextProps<IDropdown, HTMLDivElement>
|
||||
): JSX.Element => {
|
||||
const renderedList: JSX.Element[] = [];
|
||||
props.options.forEach((option: IDropdownOption) => {
|
||||
const item = (
|
||||
<div key={option.key} style={BranchesDropdownOptionContainerStyle}>
|
||||
{this.onRenderPinnedReposBranchesDropdownOption(option)}
|
||||
</div>
|
||||
);
|
||||
renderedList.push(item);
|
||||
});
|
||||
|
||||
return <>{renderedList}</>;
|
||||
};
|
||||
|
||||
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
|
||||
const item: RepoListItem = option.data;
|
||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
||||
|
||||
if (option.index === ReposListComponent.FooterIndex) {
|
||||
const linkProps: ILinkProps = {
|
||||
disabled: branchesProps.isLoading,
|
||||
onClick: branchesProps.loadMore
|
||||
};
|
||||
|
||||
return <Link {...linkProps}>{option.text}</Link>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(option.text),
|
||||
styles: BranchesDropdownCheckboxStyles,
|
||||
defaultChecked: option.selected,
|
||||
disabled: option.disabled,
|
||||
onChange: (event, checked) => {
|
||||
const repoListItem = { ...item };
|
||||
const branch: IGitHubBranch = { name: option.text };
|
||||
repoListItem.branches = repoListItem.branches.filter(element => element.name !== branch.name);
|
||||
if (checked) {
|
||||
repoListItem.branches.push(branch);
|
||||
}
|
||||
|
||||
this.props.pinRepo(repoListItem);
|
||||
}
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
}
|
||||
|
||||
private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
const linkProps: ILinkProps = {
|
||||
disabled: this.props.unpinnedReposProps.isLoading,
|
||||
onClick: this.props.unpinnedReposProps.loadMore
|
||||
};
|
||||
|
||||
const linkText = this.props.unpinnedReposProps.isLoading
|
||||
? ReposListComponent.LoadingText
|
||||
: ReposListComponent.LoadMoreText;
|
||||
return <Link {...linkProps}>{linkText}</Link>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
||||
styles: ReposListCheckboxStyles,
|
||||
onChange: () => {
|
||||
const repoListItem = { ...item };
|
||||
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
|
||||
this.props.pinRepo(repoListItem);
|
||||
}
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
};
|
||||
|
||||
private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => {
|
||||
const props: IDetailsRowBaseProps = {
|
||||
...detailsFooterProps,
|
||||
item: {},
|
||||
itemIndex: ReposListComponent.FooterIndex
|
||||
};
|
||||
|
||||
return <DetailsRow {...props} />;
|
||||
};
|
||||
|
||||
private static getCheckboxPropsForLabel(label: string): ICheckboxProps {
|
||||
return {
|
||||
label,
|
||||
title: label,
|
||||
ariaLabel: label
|
||||
};
|
||||
}
|
||||
|
||||
private static getKey(item: RepoListItem): string {
|
||||
return item.key;
|
||||
}
|
||||
}
|
||||
import {
|
||||
Checkbox,
|
||||
DetailsList,
|
||||
DetailsRow,
|
||||
Dropdown,
|
||||
ICheckboxProps,
|
||||
IDetailsFooterProps,
|
||||
IDetailsListProps,
|
||||
IDetailsRowBaseProps,
|
||||
IDropdown,
|
||||
IDropdownOption,
|
||||
IDropdownProps,
|
||||
ILinkProps,
|
||||
ISelectableDroppableTextProps,
|
||||
Link,
|
||||
ResponsiveMode,
|
||||
SelectionMode,
|
||||
Text,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { RepoListItem } from "./GitHubReposComponent";
|
||||
import {
|
||||
BranchesDropdownCheckboxStyles,
|
||||
BranchesDropdownOptionContainerStyle,
|
||||
ReposListCheckboxStyles,
|
||||
ReposListRepoColumnMinWidth,
|
||||
ReposListBranchesColumnWidth,
|
||||
BranchesDropdownWidth,
|
||||
BranchesDropdownStyles,
|
||||
} from "./GitHubStyleConstants";
|
||||
|
||||
export interface ReposListComponentProps {
|
||||
branchesProps: Record<string, BranchesProps>; // key'd by repo key
|
||||
pinnedReposProps: PinnedReposProps;
|
||||
unpinnedReposProps: UnpinnedReposProps;
|
||||
pinRepo: (repo: RepoListItem) => void;
|
||||
unpinRepo: (repo: RepoListItem) => void;
|
||||
}
|
||||
|
||||
export interface BranchesProps {
|
||||
branches: IGitHubBranch[];
|
||||
lastPageInfo?: IGitHubPageInfo;
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
export interface PinnedReposProps {
|
||||
repos: RepoListItem[];
|
||||
}
|
||||
|
||||
export interface UnpinnedReposProps {
|
||||
repos: RepoListItem[];
|
||||
hasMore: boolean;
|
||||
isLoading: boolean;
|
||||
loadMore: () => void;
|
||||
}
|
||||
|
||||
export class ReposListComponent extends React.Component<ReposListComponentProps> {
|
||||
private static readonly PinnedReposColumnName = "Pinned repos";
|
||||
private static readonly UnpinnedReposColumnName = "Unpinned repos";
|
||||
private static readonly BranchesColumnName = "Branches";
|
||||
private static readonly LoadingText = "Loading...";
|
||||
private static readonly LoadMoreText = "Load more";
|
||||
private static readonly DefaultBranchName = "master";
|
||||
private static readonly FooterIndex = -1;
|
||||
|
||||
public render(): JSX.Element {
|
||||
const pinnedReposListProps: IDetailsListProps = {
|
||||
styles: {
|
||||
contentWrapper: {
|
||||
height: this.props.pinnedReposProps.repos.length ? undefined : 0,
|
||||
},
|
||||
},
|
||||
items: this.props.pinnedReposProps.repos,
|
||||
getKey: ReposListComponent.getKey,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
columns: [
|
||||
{
|
||||
key: ReposListComponent.PinnedReposColumnName,
|
||||
name: ReposListComponent.PinnedReposColumnName,
|
||||
ariaLabel: ReposListComponent.PinnedReposColumnName,
|
||||
minWidth: ReposListRepoColumnMinWidth,
|
||||
onRender: this.onRenderPinnedReposColumnItem,
|
||||
},
|
||||
{
|
||||
key: ReposListComponent.BranchesColumnName,
|
||||
name: ReposListComponent.BranchesColumnName,
|
||||
ariaLabel: ReposListComponent.BranchesColumnName,
|
||||
minWidth: ReposListBranchesColumnWidth,
|
||||
maxWidth: ReposListBranchesColumnWidth,
|
||||
onRender: this.onRenderPinnedReposBranchesColumnItem,
|
||||
},
|
||||
],
|
||||
onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter,
|
||||
};
|
||||
|
||||
const unpinnedReposListProps: IDetailsListProps = {
|
||||
items: this.props.unpinnedReposProps.repos,
|
||||
getKey: ReposListComponent.getKey,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
columns: [
|
||||
{
|
||||
key: ReposListComponent.UnpinnedReposColumnName,
|
||||
name: ReposListComponent.UnpinnedReposColumnName,
|
||||
ariaLabel: ReposListComponent.UnpinnedReposColumnName,
|
||||
minWidth: ReposListRepoColumnMinWidth,
|
||||
onRender: this.onRenderUnpinnedReposColumnItem,
|
||||
},
|
||||
{
|
||||
key: ReposListComponent.BranchesColumnName,
|
||||
name: ReposListComponent.BranchesColumnName,
|
||||
ariaLabel: ReposListComponent.BranchesColumnName,
|
||||
minWidth: ReposListBranchesColumnWidth,
|
||||
maxWidth: ReposListBranchesColumnWidth,
|
||||
onRender: this.onRenderUnpinnedReposBranchesColumnItem,
|
||||
},
|
||||
],
|
||||
onRenderDetailsFooter:
|
||||
this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore
|
||||
? this.onRenderReposFooter
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsList {...pinnedReposListProps} />
|
||||
<DetailsList {...unpinnedReposListProps} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <Text>None</Text>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
||||
styles: ReposListCheckboxStyles,
|
||||
defaultChecked: true,
|
||||
onChange: () => this.props.unpinRepo(item),
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
};
|
||||
|
||||
private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
||||
const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({
|
||||
key: branch.name,
|
||||
text: branch.name,
|
||||
data: item,
|
||||
disabled: item.branches.length === 1 && branch.name === item.branches[0].name,
|
||||
selected: item.branches.findIndex((element) => element.name === branch.name) !== -1,
|
||||
}));
|
||||
|
||||
if (branchesProps.hasMore || branchesProps.isLoading) {
|
||||
const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText;
|
||||
options.push({
|
||||
key: text,
|
||||
text,
|
||||
data: item,
|
||||
index: ReposListComponent.FooterIndex,
|
||||
});
|
||||
}
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
styles: BranchesDropdownStyles,
|
||||
dropdownWidth: BranchesDropdownWidth,
|
||||
responsiveMode: ResponsiveMode.large,
|
||||
options,
|
||||
onRenderList: this.onRenderBranchesDropdownList,
|
||||
};
|
||||
|
||||
if (item.branches.length === 1) {
|
||||
dropdownProps.placeholder = item.branches[0].name;
|
||||
} else if (item.branches.length > 1) {
|
||||
dropdownProps.placeholder = `${item.branches.length} branches`;
|
||||
}
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
};
|
||||
|
||||
private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const dropdownProps: IDropdownProps = {
|
||||
styles: BranchesDropdownStyles,
|
||||
options: [],
|
||||
placeholder: ReposListComponent.DefaultBranchName,
|
||||
disabled: true,
|
||||
};
|
||||
|
||||
return <Dropdown {...dropdownProps} />;
|
||||
};
|
||||
|
||||
private onRenderBranchesDropdownList = (
|
||||
props: ISelectableDroppableTextProps<IDropdown, HTMLDivElement>
|
||||
): JSX.Element => {
|
||||
const renderedList: JSX.Element[] = [];
|
||||
props.options.forEach((option: IDropdownOption) => {
|
||||
const item = (
|
||||
<div key={option.key} style={BranchesDropdownOptionContainerStyle}>
|
||||
{this.onRenderPinnedReposBranchesDropdownOption(option)}
|
||||
</div>
|
||||
);
|
||||
renderedList.push(item);
|
||||
});
|
||||
|
||||
return <>{renderedList}</>;
|
||||
};
|
||||
|
||||
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
|
||||
const item: RepoListItem = option.data;
|
||||
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
|
||||
|
||||
if (option.index === ReposListComponent.FooterIndex) {
|
||||
const linkProps: ILinkProps = {
|
||||
disabled: branchesProps.isLoading,
|
||||
onClick: branchesProps.loadMore,
|
||||
};
|
||||
|
||||
return <Link {...linkProps}>{option.text}</Link>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(option.text),
|
||||
styles: BranchesDropdownCheckboxStyles,
|
||||
defaultChecked: option.selected,
|
||||
disabled: option.disabled,
|
||||
onChange: (event, checked) => {
|
||||
const repoListItem = { ...item };
|
||||
const branch: IGitHubBranch = { name: option.text };
|
||||
repoListItem.branches = repoListItem.branches.filter((element) => element.name !== branch.name);
|
||||
if (checked) {
|
||||
repoListItem.branches.push(branch);
|
||||
}
|
||||
|
||||
this.props.pinRepo(repoListItem);
|
||||
},
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
}
|
||||
|
||||
private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
|
||||
if (index === ReposListComponent.FooterIndex) {
|
||||
const linkProps: ILinkProps = {
|
||||
disabled: this.props.unpinnedReposProps.isLoading,
|
||||
onClick: this.props.unpinnedReposProps.loadMore,
|
||||
};
|
||||
|
||||
const linkText = this.props.unpinnedReposProps.isLoading
|
||||
? ReposListComponent.LoadingText
|
||||
: ReposListComponent.LoadMoreText;
|
||||
return <Link {...linkProps}>{linkText}</Link>;
|
||||
}
|
||||
|
||||
const checkboxProps: ICheckboxProps = {
|
||||
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
|
||||
styles: ReposListCheckboxStyles,
|
||||
onChange: () => {
|
||||
const repoListItem = { ...item };
|
||||
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
|
||||
this.props.pinRepo(repoListItem);
|
||||
},
|
||||
};
|
||||
|
||||
return <Checkbox {...checkboxProps} />;
|
||||
};
|
||||
|
||||
private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => {
|
||||
const props: IDetailsRowBaseProps = {
|
||||
...detailsFooterProps,
|
||||
item: {},
|
||||
itemIndex: ReposListComponent.FooterIndex,
|
||||
};
|
||||
|
||||
return <DetailsRow {...props} />;
|
||||
};
|
||||
|
||||
private static getCheckboxPropsForLabel(label: string): ICheckboxProps {
|
||||
return {
|
||||
label,
|
||||
title: label,
|
||||
ariaLabel: label,
|
||||
};
|
||||
}
|
||||
|
||||
private static getKey(item: RepoListItem): string {
|
||||
return item.key;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import * as React from "react";
|
||||
import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "office-ui-fabric-react";
|
||||
|
||||
export class GalleryHeaderComponent extends React.Component {
|
||||
private static readonly azureText = "Microsoft Azure";
|
||||
private static readonly cosmosdbText = "Cosmos DB";
|
||||
private static readonly galleryText = "Gallery";
|
||||
private static readonly loginText = "Sign In";
|
||||
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
||||
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
||||
private static readonly headerItemStyle: React.CSSProperties = {
|
||||
color: "white"
|
||||
};
|
||||
private static readonly mainHeaderTextProps: ITextProps = {
|
||||
style: GalleryHeaderComponent.headerItemStyle,
|
||||
variant: "mediumPlus",
|
||||
styles: {
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold
|
||||
}
|
||||
}
|
||||
};
|
||||
private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle };
|
||||
|
||||
private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => {
|
||||
return (
|
||||
<CommandButton onClick={onClick} ariaLabel={text}>
|
||||
<Text {...textProps}>{text}</Text>
|
||||
</CommandButton>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack
|
||||
tokens={{ childrenGap: 10 }}
|
||||
horizontal
|
||||
styles={{ root: { background: "#0078d4", paddingLeft: 20, paddingRight: 20 } }}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.azureText,
|
||||
GalleryHeaderComponent.openPortal,
|
||||
GalleryHeaderComponent.mainHeaderTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Separator vertical />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.cosmosdbText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<FontIcon style={GalleryHeaderComponent.headerItemStyle} iconName="ChevronRight" />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.galleryText,
|
||||
undefined,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.loginText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
import * as React from "react";
|
||||
import { Stack, Text, Separator, FontIcon, CommandButton, FontWeights, ITextProps } from "office-ui-fabric-react";
|
||||
|
||||
export class GalleryHeaderComponent extends React.Component {
|
||||
private static readonly azureText = "Microsoft Azure";
|
||||
private static readonly cosmosdbText = "Cosmos DB";
|
||||
private static readonly galleryText = "Gallery";
|
||||
private static readonly loginText = "Sign In";
|
||||
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
||||
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
||||
private static readonly headerItemStyle: React.CSSProperties = {
|
||||
color: "white",
|
||||
};
|
||||
private static readonly mainHeaderTextProps: ITextProps = {
|
||||
style: GalleryHeaderComponent.headerItemStyle,
|
||||
variant: "mediumPlus",
|
||||
styles: {
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
},
|
||||
},
|
||||
};
|
||||
private static readonly headerItemTextProps: ITextProps = { style: GalleryHeaderComponent.headerItemStyle };
|
||||
|
||||
private renderHeaderItem = (text: string, onClick: () => void, textProps: ITextProps): JSX.Element => {
|
||||
return (
|
||||
<CommandButton onClick={onClick} ariaLabel={text}>
|
||||
<Text {...textProps}>{text}</Text>
|
||||
</CommandButton>
|
||||
);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
return (
|
||||
<Stack
|
||||
tokens={{ childrenGap: 10 }}
|
||||
horizontal
|
||||
styles={{ root: { background: "#0078d4", paddingLeft: 20, paddingRight: 20 } }}
|
||||
verticalAlign="center"
|
||||
>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.azureText,
|
||||
GalleryHeaderComponent.openPortal,
|
||||
GalleryHeaderComponent.mainHeaderTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<Separator vertical />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.cosmosdbText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<FontIcon style={GalleryHeaderComponent.headerItemStyle} iconName="ChevronRight" />
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.galleryText,
|
||||
undefined,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
<Stack.Item grow>
|
||||
<></>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.loginText,
|
||||
GalleryHeaderComponent.openDataExplorer,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,186 +1,186 @@
|
||||
/**
|
||||
* How to use this component:
|
||||
*
|
||||
* In your html markup, use:
|
||||
* <input-typeahead params="{
|
||||
choices:choices,
|
||||
selection:selection,
|
||||
inputValue:inputValue,
|
||||
placeholder:'Enter source',
|
||||
typeaheadOverrideOptions:typeaheadOverrideOptions
|
||||
}"></input-typeahead>
|
||||
* The parameters are documented below.
|
||||
*
|
||||
* Notes:
|
||||
* - dynamic:true by default, this allows choices to change after initialization.
|
||||
* To turn it off, use:
|
||||
* typeaheadOverrideOptions: { dynamic:false }
|
||||
*
|
||||
*/
|
||||
|
||||
import "jquery-typeahead";
|
||||
import template from "./input-typeahead.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class InputTypeaheadComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: InputTypeaheadViewModel,
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
caption: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface InputTypeaheadParams {
|
||||
/**
|
||||
* List of choices available in the dropdown.
|
||||
*/
|
||||
choices: ko.ObservableArray<Item>;
|
||||
|
||||
/**
|
||||
* Gets updated when user clicks on the choice in the dropdown
|
||||
*/
|
||||
selection?: ko.Observable<Item>;
|
||||
|
||||
/**
|
||||
* The current string value of <input>
|
||||
*/
|
||||
inputValue?: ko.Observable<string>;
|
||||
|
||||
/**
|
||||
* Define what text you want as the input placeholder
|
||||
*/
|
||||
placeholder: string;
|
||||
|
||||
/**
|
||||
* Override default jquery-typeahead options
|
||||
* WARNING: do not override input, source or callback to avoid breaking the components behavior.
|
||||
*/
|
||||
typeaheadOverrideOptions?: any;
|
||||
|
||||
/**
|
||||
* This function gets called when pressing ENTER on the input box
|
||||
*/
|
||||
submitFct?: (inputValue: string | null, selection: Item | null) => void;
|
||||
|
||||
/**
|
||||
* Typehead comes with a Search button that we normally remove.
|
||||
* If you want to use it, turn this on
|
||||
*/
|
||||
showSearchButton?: boolean;
|
||||
}
|
||||
|
||||
interface OnClickItem {
|
||||
matchedKey: string;
|
||||
value: any;
|
||||
caption: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string | null;
|
||||
selection: Item | null;
|
||||
}
|
||||
|
||||
class InputTypeaheadViewModel {
|
||||
private static instanceCount = 0; // Generate unique id for each component's typeahead instance
|
||||
private instanceNumber: number;
|
||||
private params: InputTypeaheadParams;
|
||||
|
||||
private cache: Cache;
|
||||
|
||||
public constructor(params: InputTypeaheadParams) {
|
||||
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Must execute once ko is rendered, so that it can find the input element by id
|
||||
*/
|
||||
private initializeTypeahead() {
|
||||
let params = this.params;
|
||||
let cache = this.cache;
|
||||
let options: any = {
|
||||
input: `#${this.getComponentId()}`, //'.input-typeahead',
|
||||
order: "asc",
|
||||
minLength: 0,
|
||||
searchOnFocus: true,
|
||||
source: {
|
||||
display: "caption",
|
||||
data: () => {
|
||||
return this.params.choices();
|
||||
}
|
||||
},
|
||||
callback: {
|
||||
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
|
||||
cache.selection = item;
|
||||
|
||||
if (params.selection) {
|
||||
params.selection(item);
|
||||
}
|
||||
},
|
||||
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
|
||||
cache.inputValue = query;
|
||||
if (params.inputValue) {
|
||||
params.inputValue(query);
|
||||
}
|
||||
}
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
return item.caption === item.value
|
||||
? "<span>{{caption}}</span>"
|
||||
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
|
||||
},
|
||||
dynamic: true
|
||||
};
|
||||
|
||||
// Override options
|
||||
if (params.typeaheadOverrideOptions) {
|
||||
for (let p in params.typeaheadOverrideOptions) {
|
||||
options[p] = params.typeaheadOverrideOptions[p];
|
||||
}
|
||||
}
|
||||
|
||||
($ as any).typeahead(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this component id
|
||||
* @return unique id per instance
|
||||
*/
|
||||
private getComponentId(): string {
|
||||
return `input-typeahead${this.instanceNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed once ko is done rendering bindings
|
||||
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
||||
* Another way is to call it within setTimeout() in constructor.
|
||||
*/
|
||||
public afterRender(): void {
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
|
||||
public submit(): void {
|
||||
if (this.params.submitFct) {
|
||||
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* How to use this component:
|
||||
*
|
||||
* In your html markup, use:
|
||||
* <input-typeahead params="{
|
||||
choices:choices,
|
||||
selection:selection,
|
||||
inputValue:inputValue,
|
||||
placeholder:'Enter source',
|
||||
typeaheadOverrideOptions:typeaheadOverrideOptions
|
||||
}"></input-typeahead>
|
||||
* The parameters are documented below.
|
||||
*
|
||||
* Notes:
|
||||
* - dynamic:true by default, this allows choices to change after initialization.
|
||||
* To turn it off, use:
|
||||
* typeaheadOverrideOptions: { dynamic:false }
|
||||
*
|
||||
*/
|
||||
|
||||
import "jquery-typeahead";
|
||||
import template from "./input-typeahead.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class InputTypeaheadComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: InputTypeaheadViewModel,
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface Item {
|
||||
caption: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
interface InputTypeaheadParams {
|
||||
/**
|
||||
* List of choices available in the dropdown.
|
||||
*/
|
||||
choices: ko.ObservableArray<Item>;
|
||||
|
||||
/**
|
||||
* Gets updated when user clicks on the choice in the dropdown
|
||||
*/
|
||||
selection?: ko.Observable<Item>;
|
||||
|
||||
/**
|
||||
* The current string value of <input>
|
||||
*/
|
||||
inputValue?: ko.Observable<string>;
|
||||
|
||||
/**
|
||||
* Define what text you want as the input placeholder
|
||||
*/
|
||||
placeholder: string;
|
||||
|
||||
/**
|
||||
* Override default jquery-typeahead options
|
||||
* WARNING: do not override input, source or callback to avoid breaking the components behavior.
|
||||
*/
|
||||
typeaheadOverrideOptions?: any;
|
||||
|
||||
/**
|
||||
* This function gets called when pressing ENTER on the input box
|
||||
*/
|
||||
submitFct?: (inputValue: string | null, selection: Item | null) => void;
|
||||
|
||||
/**
|
||||
* Typehead comes with a Search button that we normally remove.
|
||||
* If you want to use it, turn this on
|
||||
*/
|
||||
showSearchButton?: boolean;
|
||||
}
|
||||
|
||||
interface OnClickItem {
|
||||
matchedKey: string;
|
||||
value: any;
|
||||
caption: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
interface Cache {
|
||||
inputValue: string | null;
|
||||
selection: Item | null;
|
||||
}
|
||||
|
||||
class InputTypeaheadViewModel {
|
||||
private static instanceCount = 0; // Generate unique id for each component's typeahead instance
|
||||
private instanceNumber: number;
|
||||
private params: InputTypeaheadParams;
|
||||
|
||||
private cache: Cache;
|
||||
|
||||
public constructor(params: InputTypeaheadParams) {
|
||||
this.instanceNumber = InputTypeaheadViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.choices.subscribe(this.initializeTypeahead.bind(this));
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Must execute once ko is rendered, so that it can find the input element by id
|
||||
*/
|
||||
private initializeTypeahead() {
|
||||
let params = this.params;
|
||||
let cache = this.cache;
|
||||
let options: any = {
|
||||
input: `#${this.getComponentId()}`, //'.input-typeahead',
|
||||
order: "asc",
|
||||
minLength: 0,
|
||||
searchOnFocus: true,
|
||||
source: {
|
||||
display: "caption",
|
||||
data: () => {
|
||||
return this.params.choices();
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
|
||||
cache.selection = item;
|
||||
|
||||
if (params.selection) {
|
||||
params.selection(item);
|
||||
}
|
||||
},
|
||||
onResult(node: any, query: any, result: any, resultCount: any, resultCountPerGroup: any) {
|
||||
cache.inputValue = query;
|
||||
if (params.inputValue) {
|
||||
params.inputValue(query);
|
||||
}
|
||||
},
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
return item.caption === item.value
|
||||
? "<span>{{caption}}</span>"
|
||||
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
|
||||
},
|
||||
dynamic: true,
|
||||
};
|
||||
|
||||
// Override options
|
||||
if (params.typeaheadOverrideOptions) {
|
||||
for (let p in params.typeaheadOverrideOptions) {
|
||||
options[p] = params.typeaheadOverrideOptions[p];
|
||||
}
|
||||
}
|
||||
|
||||
($ as any).typeahead(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get this component id
|
||||
* @return unique id per instance
|
||||
*/
|
||||
private getComponentId(): string {
|
||||
return `input-typeahead${this.instanceNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed once ko is done rendering bindings
|
||||
* Use ko's "template: afterRender" callback to do that without actually using any template.
|
||||
* Another way is to call it within setTimeout() in constructor.
|
||||
*/
|
||||
public afterRender(): void {
|
||||
this.initializeTypeahead();
|
||||
}
|
||||
|
||||
public submit(): void {
|
||||
if (this.params.submitFct) {
|
||||
this.params.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ describe("inputTypeahead", () => {
|
||||
const props: InputTypeaheadComponentProps = {
|
||||
choices: [
|
||||
{ caption: "item1", value: "value1" },
|
||||
{ caption: "item2", value: "value2" }
|
||||
{ caption: "item2", value: "value2" },
|
||||
],
|
||||
placeholder: "placeholder",
|
||||
useTextarea: false
|
||||
useTextarea: false,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<InputTypeaheadComponent {...props} />);
|
||||
@@ -22,10 +22,10 @@ describe("inputTypeahead", () => {
|
||||
const props: InputTypeaheadComponentProps = {
|
||||
choices: [
|
||||
{ caption: "item1", value: "value1" },
|
||||
{ caption: "item2", value: "value2" }
|
||||
{ caption: "item2", value: "value2" },
|
||||
],
|
||||
placeholder: "placeholder",
|
||||
useTextarea: true
|
||||
useTextarea: true,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<InputTypeaheadComponent {...props} />);
|
||||
|
||||
@@ -103,7 +103,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
super(props);
|
||||
this.cache = {
|
||||
inputValue: null,
|
||||
selection: null
|
||||
selection: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
className="input-typehead"
|
||||
onKeyDown={(event: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(event)}
|
||||
>
|
||||
<div className="typeahead__container" ref={input => (this.containerElt = input)}>
|
||||
<div className="typeahead__container" ref={(input) => (this.containerElt = input)}>
|
||||
<div className="typeahead__field">
|
||||
<span className="typeahead__query">
|
||||
{this.props.useTextarea ? (
|
||||
@@ -147,7 +147,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
name="q"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={input => (this.inputElt = input)}
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
) : (
|
||||
@@ -156,7 +156,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
type="search"
|
||||
autoComplete="off"
|
||||
aria-label="Input query"
|
||||
ref={input => (this.inputElt = input)}
|
||||
ref={(input) => (this.inputElt = input)}
|
||||
defaultValue={this.props.defaultValue}
|
||||
/>
|
||||
)}
|
||||
@@ -181,9 +181,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.submitFct(this.cache.inputValue, this.cache.selection);
|
||||
$(this.containerElt)
|
||||
.children(".typeahead__result")
|
||||
.hide();
|
||||
$(this.containerElt).children(".typeahead__result").hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,7 +201,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
display: "caption",
|
||||
data: () => {
|
||||
return props.choices;
|
||||
}
|
||||
},
|
||||
},
|
||||
callback: {
|
||||
onClick: (node: any, a: any, item: OnClickItem, event: any) => {
|
||||
@@ -218,7 +216,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
if (props.onNewValue) {
|
||||
props.onNewValue(query);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
template: (query: string, item: any) => {
|
||||
// Don't display id if caption *IS* the id
|
||||
@@ -226,7 +224,7 @@ export class InputTypeaheadComponent extends React.Component<
|
||||
? "<span>{{caption}}</span>"
|
||||
: "<span><div>{{caption}}</div><div><small>{{value}}</small></div></span>";
|
||||
},
|
||||
dynamic: true
|
||||
dynamic: true,
|
||||
};
|
||||
|
||||
// Override options
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<span class="input-typeahead-container">
|
||||
<form class="input-typehead" data-bind="submit:submit">
|
||||
<div class="typeahead__container">
|
||||
<div class="typeahead__field">
|
||||
<span class="typeahead__query">
|
||||
<input
|
||||
name="q"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
data-bind="attr: { placeholder: params.placeholder, id:getComponentId() }, value:params.inputValue, template: { afterRender:afterRender() }"
|
||||
/>
|
||||
</span>
|
||||
<span class="typeahead__button" data-bind="visible:params.showSearchButton">
|
||||
<button type="submit"><span class="typeahead__search-icon"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</span>
|
||||
<span class="input-typeahead-container">
|
||||
<form class="input-typehead" data-bind="submit:submit">
|
||||
<div class="typeahead__container">
|
||||
<div class="typeahead__field">
|
||||
<span class="typeahead__query">
|
||||
<input
|
||||
name="q"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
data-bind="attr: { placeholder: params.placeholder, id:getComponentId() }, value:params.inputValue, template: { afterRender:afterRender() }"
|
||||
/>
|
||||
</span>
|
||||
<span class="typeahead__button" data-bind="visible:params.showSearchButton">
|
||||
<button type="submit"><span class="typeahead__search-icon"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</span>
|
||||
|
||||
@@ -1,173 +1,173 @@
|
||||
import Q from "q";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import template from "./json-editor-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class JsonEditorComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: JsonEditorViewModel,
|
||||
template
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
export interface JsonEditorParams {
|
||||
content: ViewModels.Editable<string>; // Sets the initial content of the editor
|
||||
isReadOnly: boolean;
|
||||
ariaLabel: string; // Sets what will be read to the user to define the control
|
||||
updatedContent?: ViewModels.Editable<string>; // Gets updated when user edits
|
||||
selectedContent?: ViewModels.Editable<string>; // Gets updated when user selects content from the editor
|
||||
lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"];
|
||||
theme?: string; // Monaco editor theme
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Editor:
|
||||
* A ko wrapper for the Monaco editor
|
||||
*
|
||||
* How to use in your markup:
|
||||
* <json-editor params="{ isReadOnly:true, content:myJsonString, ariaLabel: myDescriptiveAriaLabel }"></json-editor>
|
||||
*
|
||||
* In writable mode, if you want to get changes to the content pass updatedContent and subscribe to it.
|
||||
* content and updateContent are different to prevent circular updates.
|
||||
*/
|
||||
export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
protected editorContainer: HTMLElement;
|
||||
protected params: JsonEditorParams;
|
||||
private static instanceCount = 0; // Generate unique id to get different monaco editor
|
||||
private editor: monaco.editor.IStandaloneCodeEditor;
|
||||
private instanceNumber: number;
|
||||
private resizer: EventListenerOrEventListenerObject;
|
||||
private observer: MutationObserver;
|
||||
private offsetWidth: number;
|
||||
private offsetHeight: number;
|
||||
private selectionListener: monaco.IDisposable;
|
||||
private latestContentVersionId: number;
|
||||
|
||||
public constructor(params: JsonEditorParams) {
|
||||
super();
|
||||
|
||||
this.instanceNumber = JsonEditorViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.content.subscribe((newValue: string) => {
|
||||
if (newValue) {
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => {
|
||||
if (
|
||||
this.offsetWidth !== this.editorContainer.offsetWidth ||
|
||||
this.offsetHeight !== this.editorContainer.offsetHeight
|
||||
) {
|
||||
this.editor.layout();
|
||||
this.offsetWidth = this.editorContainer.offsetWidth;
|
||||
this.offsetHeight = this.editorContainer.offsetHeight;
|
||||
}
|
||||
};
|
||||
this.observer = new MutationObserver(onObserve);
|
||||
}
|
||||
|
||||
protected getEditorId(): string {
|
||||
return `jsoneditor${this.instanceNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
this.registerCompletionItemProvider();
|
||||
this.editorContainer = document.getElementById(this.getEditorId());
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
value: content,
|
||||
language: this.getEditorLanguage(),
|
||||
readOnly: this.params.isReadOnly,
|
||||
lineNumbers: this.params.lineNumbers || "off",
|
||||
fontSize: 12,
|
||||
ariaLabel: this.params.ariaLabel,
|
||||
theme: this.params.theme
|
||||
};
|
||||
|
||||
this.editorContainer.innerHTML = "";
|
||||
createCallback(monaco.editor.create(this.editorContainer, options));
|
||||
}
|
||||
|
||||
// Interface. Will be implemented in children editor view model such as EditorViewModel.
|
||||
protected registerCompletionItemProvider() {}
|
||||
|
||||
// Interface. Will be implemented in children editor view model such as EditorViewModel.
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
return Q.Promise(() => {});
|
||||
}
|
||||
|
||||
protected getEditorLanguage(): string {
|
||||
return "json";
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.params.isReadOnly && this.params.updatedContent) {
|
||||
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.params.updatedContent(queryEditorModel.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
if (this.params.selectedContent) {
|
||||
this.selectionListener = this.editor.onDidChangeCursorSelection(
|
||||
(event: monaco.editor.ICursorSelectionChangedEvent) => {
|
||||
const selectedContent: string = this.editor.getModel().getValueInRange(event.selection);
|
||||
this.params.selectedContent(selectedContent);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.resizer = () => {
|
||||
editor.layout();
|
||||
};
|
||||
window.addEventListener("resize", this.resizer);
|
||||
|
||||
this.offsetHeight = this.editorContainer.offsetHeight;
|
||||
this.offsetWidth = this.editorContainer.offsetWidth;
|
||||
|
||||
this.observer.observe(document.body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
childList: true
|
||||
});
|
||||
|
||||
this.editor.getModel().onDidChangeContent(async (e: monaco.editor.IModelContentChangedEvent) => {
|
||||
if (!(<any>e).isFlush) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.latestContentVersionId = e.versionId;
|
||||
let input = (<any>e).changes[0].text;
|
||||
let marks = await this.getErrorMarkers(input);
|
||||
if (e.versionId === this.latestContentVersionId) {
|
||||
monaco.editor.setModelMarkers(this.editor.getModel(), "ErrorMarkerOwner", marks);
|
||||
}
|
||||
});
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
private dispose() {
|
||||
window.removeEventListener("resize", this.resizer);
|
||||
this.selectionListener && this.selectionListener.dispose();
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
import Q from "q";
|
||||
import * as monaco from "monaco-editor";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { WaitsForTemplateViewModel } from "../../WaitsForTemplateViewModel";
|
||||
import template from "./json-editor-component.html";
|
||||
|
||||
/**
|
||||
* Helper class for ko component registration
|
||||
*/
|
||||
export class JsonEditorComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: JsonEditorViewModel,
|
||||
template,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters for this component
|
||||
*/
|
||||
export interface JsonEditorParams {
|
||||
content: ViewModels.Editable<string>; // Sets the initial content of the editor
|
||||
isReadOnly: boolean;
|
||||
ariaLabel: string; // Sets what will be read to the user to define the control
|
||||
updatedContent?: ViewModels.Editable<string>; // Gets updated when user edits
|
||||
selectedContent?: ViewModels.Editable<string>; // Gets updated when user selects content from the editor
|
||||
lineNumbers?: monaco.editor.IEditorOptions["lineNumbers"];
|
||||
theme?: string; // Monaco editor theme
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Editor:
|
||||
* A ko wrapper for the Monaco editor
|
||||
*
|
||||
* How to use in your markup:
|
||||
* <json-editor params="{ isReadOnly:true, content:myJsonString, ariaLabel: myDescriptiveAriaLabel }"></json-editor>
|
||||
*
|
||||
* In writable mode, if you want to get changes to the content pass updatedContent and subscribe to it.
|
||||
* content and updateContent are different to prevent circular updates.
|
||||
*/
|
||||
export class JsonEditorViewModel extends WaitsForTemplateViewModel {
|
||||
protected editorContainer: HTMLElement;
|
||||
protected params: JsonEditorParams;
|
||||
private static instanceCount = 0; // Generate unique id to get different monaco editor
|
||||
private editor: monaco.editor.IStandaloneCodeEditor;
|
||||
private instanceNumber: number;
|
||||
private resizer: EventListenerOrEventListenerObject;
|
||||
private observer: MutationObserver;
|
||||
private offsetWidth: number;
|
||||
private offsetHeight: number;
|
||||
private selectionListener: monaco.IDisposable;
|
||||
private latestContentVersionId: number;
|
||||
|
||||
public constructor(params: JsonEditorParams) {
|
||||
super();
|
||||
|
||||
this.instanceNumber = JsonEditorViewModel.instanceCount++;
|
||||
this.params = params;
|
||||
|
||||
this.params.content.subscribe((newValue: string) => {
|
||||
if (newValue) {
|
||||
if (!!this.editor) {
|
||||
this.editor.getModel().setValue(newValue);
|
||||
} else {
|
||||
this.createEditor(newValue, this.configureEditor.bind(this));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const onObserve: MutationCallback = (mutations: MutationRecord[], observer: MutationObserver): void => {
|
||||
if (
|
||||
this.offsetWidth !== this.editorContainer.offsetWidth ||
|
||||
this.offsetHeight !== this.editorContainer.offsetHeight
|
||||
) {
|
||||
this.editor.layout();
|
||||
this.offsetWidth = this.editorContainer.offsetWidth;
|
||||
this.offsetHeight = this.editorContainer.offsetHeight;
|
||||
}
|
||||
};
|
||||
this.observer = new MutationObserver(onObserve);
|
||||
}
|
||||
|
||||
protected getEditorId(): string {
|
||||
return `jsoneditor${this.instanceNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the monaco editor and attach to DOM
|
||||
*/
|
||||
protected createEditor(content: string, createCallback: (e: monaco.editor.IStandaloneCodeEditor) => void) {
|
||||
this.registerCompletionItemProvider();
|
||||
this.editorContainer = document.getElementById(this.getEditorId());
|
||||
const options: monaco.editor.IEditorConstructionOptions = {
|
||||
value: content,
|
||||
language: this.getEditorLanguage(),
|
||||
readOnly: this.params.isReadOnly,
|
||||
lineNumbers: this.params.lineNumbers || "off",
|
||||
fontSize: 12,
|
||||
ariaLabel: this.params.ariaLabel,
|
||||
theme: this.params.theme,
|
||||
};
|
||||
|
||||
this.editorContainer.innerHTML = "";
|
||||
createCallback(monaco.editor.create(this.editorContainer, options));
|
||||
}
|
||||
|
||||
// Interface. Will be implemented in children editor view model such as EditorViewModel.
|
||||
protected registerCompletionItemProvider() {}
|
||||
|
||||
// Interface. Will be implemented in children editor view model such as EditorViewModel.
|
||||
protected getErrorMarkers(input: string): Q.Promise<monaco.editor.IMarkerData[]> {
|
||||
return Q.Promise(() => {});
|
||||
}
|
||||
|
||||
protected getEditorLanguage(): string {
|
||||
return "json";
|
||||
}
|
||||
|
||||
protected configureEditor(editor: monaco.editor.IStandaloneCodeEditor) {
|
||||
this.editor = editor;
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
if (!this.params.isReadOnly && this.params.updatedContent) {
|
||||
queryEditorModel.onDidChangeContent((e: monaco.editor.IModelContentChangedEvent) => {
|
||||
const queryEditorModel = this.editor.getModel();
|
||||
this.params.updatedContent(queryEditorModel.getValue());
|
||||
});
|
||||
}
|
||||
|
||||
if (this.params.selectedContent) {
|
||||
this.selectionListener = this.editor.onDidChangeCursorSelection(
|
||||
(event: monaco.editor.ICursorSelectionChangedEvent) => {
|
||||
const selectedContent: string = this.editor.getModel().getValueInRange(event.selection);
|
||||
this.params.selectedContent(selectedContent);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.resizer = () => {
|
||||
editor.layout();
|
||||
};
|
||||
window.addEventListener("resize", this.resizer);
|
||||
|
||||
this.offsetHeight = this.editorContainer.offsetHeight;
|
||||
this.offsetWidth = this.editorContainer.offsetWidth;
|
||||
|
||||
this.observer.observe(document.body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
childList: true,
|
||||
});
|
||||
|
||||
this.editor.getModel().onDidChangeContent(async (e: monaco.editor.IModelContentChangedEvent) => {
|
||||
if (!(<any>e).isFlush) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.latestContentVersionId = e.versionId;
|
||||
let input = (<any>e).changes[0].text;
|
||||
let marks = await this.getErrorMarkers(input);
|
||||
if (e.versionId === this.latestContentVersionId) {
|
||||
monaco.editor.setModelMarkers(this.editor.getModel(), "ErrorMarkerOwner", marks);
|
||||
}
|
||||
});
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
private dispose() {
|
||||
window.removeEventListener("resize", this.resizer);
|
||||
this.selectionListener && this.selectionListener.dispose();
|
||||
this.observer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<div class="jsonEditor" data-bind="attr:{ id:getEditorId() }"></div>
|
||||
<div class="jsonEditor" data-bind="attr:{ id:getEditorId() }"></div>
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
|
||||
|
||||
const createTestDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType"
|
||||
};
|
||||
};
|
||||
|
||||
const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType"
|
||||
};
|
||||
};
|
||||
|
||||
const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/"
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType"
|
||||
};
|
||||
};
|
||||
|
||||
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
||||
documentEndpoint: null,
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType"
|
||||
};
|
||||
};
|
||||
|
||||
const createTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/"
|
||||
},
|
||||
databaseAccount: createTestDatabaseAccount()
|
||||
});
|
||||
};
|
||||
|
||||
const createMongo32Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo"
|
||||
},
|
||||
databaseAccount: createTestMongo32DatabaseAccount()
|
||||
});
|
||||
};
|
||||
|
||||
const createMongo36Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo"
|
||||
},
|
||||
databaseAccount: createTestMongo36DatabaseAccount()
|
||||
});
|
||||
};
|
||||
|
||||
const createCassandraTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra"
|
||||
},
|
||||
databaseAccount: createTestCassandraDatabaseAccount()
|
||||
});
|
||||
};
|
||||
|
||||
describe("NotebookTerminalComponent", () => {
|
||||
it("getTerminalParams: Test for terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([["terminal", "true"]])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo32Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host]
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo36Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host]
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Cassandra terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createCassandraTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host]
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import { NotebookTerminalComponent } from "./NotebookTerminalComponent";
|
||||
|
||||
const createTestDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType",
|
||||
};
|
||||
};
|
||||
|
||||
const createTestMongo32DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType",
|
||||
};
|
||||
};
|
||||
|
||||
const createTestMongo36DatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: null,
|
||||
documentEndpoint: "https://testDocumentEndpoint.azure.com/",
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
mongoEndpoint: "https://testMongoEndpoint.azure.com/",
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType",
|
||||
};
|
||||
};
|
||||
|
||||
const createTestCassandraDatabaseAccount = (): DataModels.DatabaseAccount => {
|
||||
return {
|
||||
id: "testId",
|
||||
kind: "testKind",
|
||||
location: "testLocation",
|
||||
name: "testName",
|
||||
properties: {
|
||||
cassandraEndpoint: "https://testCassandraEndpoint.azure.com/",
|
||||
documentEndpoint: null,
|
||||
gremlinEndpoint: null,
|
||||
tableEndpoint: null,
|
||||
},
|
||||
tags: "testTags",
|
||||
type: "testType",
|
||||
};
|
||||
};
|
||||
|
||||
const createTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/",
|
||||
},
|
||||
databaseAccount: createTestDatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const createMongo32Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
},
|
||||
databaseAccount: createTestMongo32DatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const createMongo36Terminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/mongo",
|
||||
},
|
||||
databaseAccount: createTestMongo36DatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
const createCassandraTerminal = (): NotebookTerminalComponent => {
|
||||
return new NotebookTerminalComponent({
|
||||
notebookServerInfo: {
|
||||
authToken: "testAuthToken",
|
||||
notebookServerEndpoint: "https://testNotebookServerEndpoint.azure.com/cassandra",
|
||||
},
|
||||
databaseAccount: createTestCassandraDatabaseAccount(),
|
||||
});
|
||||
};
|
||||
|
||||
describe("NotebookTerminalComponent", () => {
|
||||
it("getTerminalParams: Test for terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([["terminal", "true"]])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.2 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo32Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.documentEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Mongo 3.6 terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createMongo36Terminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.mongoEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it("getTerminalParams: Test for Cassandra terminal", () => {
|
||||
const terminal: NotebookTerminalComponent = createCassandraTerminal();
|
||||
const params: Map<string, string> = terminal.getTerminalParams();
|
||||
|
||||
expect(params).toEqual(
|
||||
new Map<string, string>([
|
||||
["terminal", "true"],
|
||||
["terminalEndpoint", new URL(terminal.props.databaseAccount.properties.cassandraEndpoint).host],
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("GalleryCardComponent", () => {
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
pendingScanJobIds: undefined,
|
||||
},
|
||||
isFavorite: false,
|
||||
showDownload: true,
|
||||
@@ -30,7 +30,7 @@ describe("GalleryCardComponent", () => {
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onDownloadClick: undefined,
|
||||
onDeleteClick: undefined
|
||||
onDeleteClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryCardComponent {...props} />);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Button,
|
||||
LinkBase,
|
||||
Separator,
|
||||
TooltipHost
|
||||
TooltipHost,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
@@ -47,7 +47,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
day: "numeric",
|
||||
};
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
|
||||
@@ -57,7 +57,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
aria-label={cardTitle}
|
||||
data-is-focusable="true"
|
||||
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
||||
onClick={event => this.onClick(event, this.props.onClick)}
|
||||
onClick={(event) => this.onClick(event, this.props.onClick)}
|
||||
>
|
||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
||||
<Persona
|
||||
@@ -81,7 +81,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
<Text variant="small" nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={event => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
@@ -92,8 +92,8 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
paddingTop: GalleryCardComponent.cardItemGapSmall,
|
||||
paddingBottom: GalleryCardComponent.cardItemGapSmall
|
||||
}
|
||||
paddingBottom: GalleryCardComponent.cardItemGapSmall,
|
||||
},
|
||||
}}
|
||||
nowrap
|
||||
>
|
||||
@@ -117,8 +117,8 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: GalleryCardComponent.cardItemGapBig,
|
||||
marginRight: GalleryCardComponent.cardItemGapBig
|
||||
}
|
||||
marginRight: GalleryCardComponent.cardItemGapBig,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
@@ -176,7 +176,7 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
iconProps={{ iconName }}
|
||||
title={title}
|
||||
ariaLabel={title}
|
||||
onClick={event => this.onClick(event, activate)}
|
||||
onClick={(event) => this.onClick(event, activate)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
);
|
||||
|
||||
@@ -12,11 +12,11 @@ describe("CodeOfConductComponent", () => {
|
||||
const junoClient = new JunoClient(undefined);
|
||||
junoClient.acceptCodeOfConduct = jest.fn().mockReturnValue({
|
||||
status: HttpStatusCodes.OK,
|
||||
data: true
|
||||
data: true,
|
||||
});
|
||||
codeOfConductProps = {
|
||||
junoClient: junoClient,
|
||||
onAcceptCodeOfConduct: jest.fn()
|
||||
onAcceptCodeOfConduct: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -27,10 +27,7 @@ describe("CodeOfConductComponent", () => {
|
||||
|
||||
it("onAcceptedCodeOfConductCalled", async () => {
|
||||
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||
wrapper
|
||||
.find(".genericPaneSubmitBtn")
|
||||
.first()
|
||||
.simulate("click");
|
||||
wrapper.find(".genericPaneSubmitBtn").first().simulate("click");
|
||||
await Promise.resolve();
|
||||
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
readCodeOfConduct: false
|
||||
readCodeOfConduct: false,
|
||||
};
|
||||
|
||||
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
||||
@@ -81,11 +81,11 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
|
||||
styles={{
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0"
|
||||
padding: "2 0 2 0",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12
|
||||
}
|
||||
fontSize: 12,
|
||||
},
|
||||
}}
|
||||
label="I have read and accepted the code of conduct and privacy statement"
|
||||
onChange={this.onChangeCheckbox}
|
||||
|
||||
@@ -1,114 +1,114 @@
|
||||
import * as React from "react";
|
||||
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
|
||||
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface GalleryAndNotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
interface GalleryAndNotebookViewerComponentState {
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export class GalleryAndNotebookViewerComponent extends React.Component<
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponentState
|
||||
> {
|
||||
constructor(props: GalleryAndNotebookViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notebookUrl: props.notebookUrl,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.notebookUrl) {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
notebookUrl: this.state.notebookUrl,
|
||||
galleryItem: this.state.galleryItem,
|
||||
isFavorite: this.state.isFavorite,
|
||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||
onBackClick: this.onBackClick,
|
||||
onTagClick: this.loadTaggedItems
|
||||
};
|
||||
|
||||
return <NotebookViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
selectedTab: this.state.selectedTab,
|
||||
sortBy: this.state.sortBy,
|
||||
searchText: this.state.searchText,
|
||||
openNotebook: this.openNotebook,
|
||||
onSelectedTabChange: this.onSelectedTabChange,
|
||||
onSortByChange: this.onSortByChange,
|
||||
onSearchTextChange: this.onSearchTextChange
|
||||
};
|
||||
|
||||
return <GalleryViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
private onBackClick = (): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined
|
||||
});
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
searchText: tag
|
||||
});
|
||||
};
|
||||
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
this.setState({
|
||||
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
galleryItem: data,
|
||||
isFavorite
|
||||
});
|
||||
};
|
||||
|
||||
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
|
||||
this.setState({
|
||||
selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
private onSortByChange = (sortBy: SortBy): void => {
|
||||
this.setState({
|
||||
sortBy
|
||||
});
|
||||
};
|
||||
|
||||
private onSearchTextChange = (searchText: string): void => {
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
};
|
||||
}
|
||||
import * as React from "react";
|
||||
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
|
||||
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export interface GalleryAndNotebookViewerComponentProps {
|
||||
container?: Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
interface GalleryAndNotebookViewerComponentState {
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export class GalleryAndNotebookViewerComponent extends React.Component<
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponentState
|
||||
> {
|
||||
constructor(props: GalleryAndNotebookViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notebookUrl: props.notebookUrl,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.notebookUrl) {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
notebookUrl: this.state.notebookUrl,
|
||||
galleryItem: this.state.galleryItem,
|
||||
isFavorite: this.state.isFavorite,
|
||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||
onBackClick: this.onBackClick,
|
||||
onTagClick: this.loadTaggedItems,
|
||||
};
|
||||
|
||||
return <NotebookViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
selectedTab: this.state.selectedTab,
|
||||
sortBy: this.state.sortBy,
|
||||
searchText: this.state.searchText,
|
||||
openNotebook: this.openNotebook,
|
||||
onSelectedTabChange: this.onSelectedTabChange,
|
||||
onSortByChange: this.onSortByChange,
|
||||
onSearchTextChange: this.onSearchTextChange,
|
||||
};
|
||||
|
||||
return <GalleryViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
private onBackClick = (): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
searchText: tag,
|
||||
});
|
||||
};
|
||||
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
this.setState({
|
||||
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
galleryItem: data,
|
||||
isFavorite,
|
||||
});
|
||||
};
|
||||
|
||||
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
|
||||
this.setState({
|
||||
selectedTab,
|
||||
});
|
||||
};
|
||||
|
||||
private onSortByChange = (sortBy: SortBy): void => {
|
||||
this.setState({
|
||||
sortBy,
|
||||
});
|
||||
};
|
||||
|
||||
private onSearchTextChange = (searchText: string): void => {
|
||||
this.setState({
|
||||
searchText,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import {
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponent
|
||||
} from "./GalleryAndNotebookViewerComponent";
|
||||
|
||||
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import {
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponent,
|
||||
} from "./GalleryAndNotebookViewerComponent";
|
||||
|
||||
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("GalleryViewerComponent", () => {
|
||||
openNotebook: undefined,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
onSearchTextChange: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<GalleryViewerComponent {...props} />);
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
PivotItem,
|
||||
SearchBox,
|
||||
Stack,
|
||||
Text
|
||||
Text,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem, IJunoResponse, IPublicGalleryData, JunoClient } from "../../../Juno/JunoClient";
|
||||
@@ -44,14 +44,14 @@ export enum GalleryTab {
|
||||
OfficialSamples,
|
||||
PublicGallery,
|
||||
Favorites,
|
||||
Published
|
||||
Published,
|
||||
}
|
||||
|
||||
export enum SortBy {
|
||||
MostViewed,
|
||||
MostDownloaded,
|
||||
MostFavorited,
|
||||
MostRecent
|
||||
MostRecent,
|
||||
}
|
||||
|
||||
interface GalleryViewerComponentState {
|
||||
@@ -106,27 +106,27 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText,
|
||||
dialogProps: undefined,
|
||||
isCodeOfConductAccepted: undefined
|
||||
isCodeOfConductAccepted: undefined,
|
||||
};
|
||||
|
||||
this.sortingOptions = [
|
||||
{
|
||||
key: SortBy.MostViewed,
|
||||
text: GalleryViewerComponent.mostViewedText
|
||||
text: GalleryViewerComponent.mostViewedText,
|
||||
},
|
||||
{
|
||||
key: SortBy.MostDownloaded,
|
||||
text: GalleryViewerComponent.mostDownloadedText
|
||||
text: GalleryViewerComponent.mostDownloadedText,
|
||||
},
|
||||
{
|
||||
key: SortBy.MostRecent,
|
||||
text: GalleryViewerComponent.mostRecentText
|
||||
}
|
||||
text: GalleryViewerComponent.mostRecentText,
|
||||
},
|
||||
];
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
this.sortingOptions.push({
|
||||
key: SortBy.MostFavorited,
|
||||
text: GalleryViewerComponent.mostFavoritedText
|
||||
text: GalleryViewerComponent.mostFavoritedText,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,14 +158,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: GalleryTab[this.state.selectedTab]
|
||||
selectedKey: GalleryTab[this.state.selectedTab],
|
||||
};
|
||||
|
||||
const pivotItems = tabs.map(tab => {
|
||||
const pivotItems = tabs.map((tab) => {
|
||||
const pivotItemProps: IPivotItemProps = {
|
||||
itemKey: GalleryTab[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: GalleryUtils.getTabTitle(tab.tab)
|
||||
headerText: GalleryUtils.getTabTitle(tab.tab),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -201,7 +201,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private createSamplesTab = (tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo => {
|
||||
return {
|
||||
tab,
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
content: this.createSearchBarHeader(this.createCardsTabContent(data)),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -212,7 +212,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
): GalleryTabInfo {
|
||||
return {
|
||||
tab,
|
||||
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
|
||||
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
"You have not liked anything",
|
||||
"Like any notebook from Official Samples or Public gallery"
|
||||
)
|
||||
: this.createSearchBarHeader(this.createCardsTabContent(data))
|
||||
: this.createSearchBarHeader(this.createCardsTabContent(data)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
"You have not published anything",
|
||||
"Publish your sample notebooks to share your published work with others"
|
||||
)
|
||||
: this.createPublishedNotebooksTabContent(data)
|
||||
: this.createPublishedNotebooksTabContent(data),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -342,7 +342,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
<th>Name</th>
|
||||
<th>Policy violations</th>
|
||||
</tr>
|
||||
{data.map(item => (
|
||||
{data.map((item) => (
|
||||
<tr key={`policy-violations-tr-${item.id}`}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.policyViolations.join(", ")}</td>
|
||||
@@ -391,7 +391,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))]
|
||||
sampleNotebooks: this.sampleNotebooks && [...this.sort(sortBy, this.search(searchText, this.sampleNotebooks))],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
this.setState({
|
||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
|
||||
isCodeOfConductAccepted: this.isCodeOfConductAccepted
|
||||
isCodeOfConductAccepted: this.isCodeOfConductAccepted,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -438,8 +438,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
this.setState({
|
||||
favoriteNotebooks: this.favoriteNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.favoriteNotebooks))
|
||||
]
|
||||
...this.sort(sortBy, this.search(searchText, this.favoriteNotebooks)),
|
||||
],
|
||||
});
|
||||
|
||||
// Refresh favorite button state
|
||||
@@ -464,14 +464,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
this.setState({
|
||||
publishedNotebooks: this.publishedNotebooks && [
|
||||
...this.sort(sortBy, this.search(searchText, this.publishedNotebooks))
|
||||
]
|
||||
...this.sort(sortBy, this.search(searchText, this.publishedNotebooks)),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private search(searchText: string, data: IGalleryItem[]): IGalleryItem[] {
|
||||
if (searchText) {
|
||||
return data?.filter(item => this.isGalleryItemPresent(searchText, item));
|
||||
return data?.filter((item) => this.isGalleryItemPresent(searchText, item));
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -482,7 +482,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
|
||||
|
||||
if (item.tags) {
|
||||
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
|
||||
searchData.push(...item.tags.map((tag) => tag.toUpperCase()));
|
||||
}
|
||||
|
||||
for (const data of searchData) {
|
||||
@@ -525,7 +525,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
private replaceGalleryItem(item: IGalleryItem, items?: IGalleryItem[]): void {
|
||||
const index = items?.findIndex(value => value.id === item.id);
|
||||
const index = items?.findIndex((value) => value.id === item.id);
|
||||
if (index !== -1) {
|
||||
items?.splice(index, 1, item);
|
||||
}
|
||||
@@ -539,14 +539,14 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
return {
|
||||
height: visibleRect.height,
|
||||
itemCount: this.columnCount * this.rowCount
|
||||
itemCount: this.columnCount * this.rowCount,
|
||||
};
|
||||
};
|
||||
|
||||
private onRenderCell = (data?: IGalleryItem): JSX.Element => {
|
||||
let isFavorite: boolean;
|
||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||
isFavorite = this.favoriteNotebooks?.find(item => item.id === data.id) !== undefined;
|
||||
isFavorite = this.favoriteNotebooks?.find((item) => item.id === data.id) !== undefined;
|
||||
}
|
||||
const props: GalleryCardComponentProps = {
|
||||
data,
|
||||
@@ -558,7 +558,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
onFavoriteClick: () => this.favoriteItem(data),
|
||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||
onDownloadClick: () => this.downloadItem(data),
|
||||
onDeleteClick: () => this.deleteItem(data)
|
||||
onDeleteClick: () => this.deleteItem(data),
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -571,7 +571,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
const searchText = tag;
|
||||
this.setState({
|
||||
searchText
|
||||
searchText,
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
@@ -591,18 +591,20 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private unfavoriteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, data, (item: IGalleryItem) => {
|
||||
this.favoriteNotebooks = this.favoriteNotebooks?.filter(value => value.id !== item.id);
|
||||
this.favoriteNotebooks = this.favoriteNotebooks?.filter((value) => value.id !== item.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
|
||||
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, item => this.refreshSelectedTab(item));
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, (item) =>
|
||||
this.refreshSelectedTab(item)
|
||||
);
|
||||
};
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, item => {
|
||||
this.publishedNotebooks = this.publishedNotebooks?.filter(notebook => item.id !== notebook.id);
|
||||
GalleryUtils.deleteItem(this.props.container, this.props.junoClient, data, (item) => {
|
||||
this.publishedNotebooks = this.publishedNotebooks?.filter((notebook) => item.id !== notebook.id);
|
||||
this.refreshSelectedTab(item);
|
||||
});
|
||||
};
|
||||
@@ -612,7 +614,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
const searchText: string = undefined;
|
||||
this.setState({
|
||||
selectedTab,
|
||||
searchText
|
||||
searchText,
|
||||
});
|
||||
|
||||
this.loadTabContent(selectedTab, searchText, this.state.sortBy, false);
|
||||
@@ -622,7 +624,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private onSearchBoxChange = (event?: React.ChangeEvent<HTMLInputElement>, newValue?: string): void => {
|
||||
const searchText = newValue;
|
||||
this.setState({
|
||||
searchText
|
||||
searchText,
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, searchText, this.state.sortBy, true);
|
||||
@@ -632,7 +634,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
private onDropdownChange = (event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
const sortBy = option.key as SortBy;
|
||||
this.setState({
|
||||
sortBy
|
||||
sortBy,
|
||||
});
|
||||
|
||||
this.loadTabContent(this.state.selectedTab, this.state.searchText, sortBy, true);
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("NotebookMetadataComponent", () => {
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
pendingScanJobIds: undefined,
|
||||
},
|
||||
isFavorite: false,
|
||||
downloadButtonText: "Download",
|
||||
@@ -28,7 +28,7 @@ describe("NotebookMetadataComponent", () => {
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
onReportAbuseClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
@@ -52,7 +52,7 @@ describe("NotebookMetadataComponent", () => {
|
||||
views: 0,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined
|
||||
pendingScanJobIds: undefined,
|
||||
},
|
||||
isFavorite: true,
|
||||
downloadButtonText: "Download",
|
||||
@@ -60,7 +60,7 @@ describe("NotebookMetadataComponent", () => {
|
||||
onDownloadClick: undefined,
|
||||
onFavoriteClick: undefined,
|
||||
onUnfavoriteClick: undefined,
|
||||
onReportAbuseClick: undefined
|
||||
onReportAbuseClick: undefined,
|
||||
};
|
||||
|
||||
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PersonaSize,
|
||||
PrimaryButton,
|
||||
Stack,
|
||||
Text
|
||||
Text,
|
||||
} from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
@@ -35,7 +35,7 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
day: "numeric",
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
|
||||
@@ -44,7 +44,8 @@ interface NotebookViewerComponentState {
|
||||
showProgressBar: boolean;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
export class NotebookViewerComponent
|
||||
extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements DialogHost {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
@@ -59,12 +60,12 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
isReadOnly: true,
|
||||
cellEditorType: "monaco",
|
||||
autoSaveInterval: 365 * 24 * 3600 * 1000, // There is no way to turn off auto-save, set to 1 year
|
||||
contentProvider: contents.JupyterContentProvider // NotebookViewer only knows how to talk to Jupyter contents API
|
||||
contentProvider: contents.JupyterContentProvider, // NotebookViewer only knows how to talk to Jupyter contents API
|
||||
});
|
||||
|
||||
this.notebookComponentBootstrapper = new NotebookComponentBootstrapper({
|
||||
notebookClient: this.clientManager,
|
||||
contentRef: createContentRef()
|
||||
contentRef: createContentRef(),
|
||||
});
|
||||
|
||||
this.state = {
|
||||
@@ -72,7 +73,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
dialogProps: undefined,
|
||||
showProgressBar: true
|
||||
showProgressBar: true,
|
||||
};
|
||||
|
||||
this.loadNotebookContent();
|
||||
@@ -148,7 +149,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||
hideInputs: this.props.hideInputs,
|
||||
hidePrompts: this.props.hidePrompts
|
||||
hidePrompts: this.props.hidePrompts,
|
||||
})}
|
||||
|
||||
{this.state.dialogProps && <DialogComponent {...this.state.dialogProps} />}
|
||||
@@ -173,7 +174,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
|
||||
return {
|
||||
galleryItem,
|
||||
isFavorite
|
||||
isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,25 +206,25 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
onCancel && onCancel();
|
||||
},
|
||||
choiceGroupProps,
|
||||
textFieldProps
|
||||
}
|
||||
textFieldProps,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private favoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
GalleryUtils.favoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) =>
|
||||
this.setState({ galleryItem: item, isFavorite: true })
|
||||
);
|
||||
};
|
||||
|
||||
private unfavoriteItem = async (): Promise<void> => {
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
GalleryUtils.unfavoriteItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) =>
|
||||
this.setState({ galleryItem: item, isFavorite: false })
|
||||
);
|
||||
};
|
||||
|
||||
private downloadItem = async (): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, (item) =>
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,292 +1,292 @@
|
||||
import * as _ from "underscore";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsListProps,
|
||||
IDetailsRowProps,
|
||||
DetailsRow
|
||||
} from "office-ui-fabric-react/lib/DetailsList";
|
||||
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone";
|
||||
import { IconButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { IColumn } from "office-ui-fabric-react/lib/DetailsList";
|
||||
import { IContextualMenuProps, ContextualMenu } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import {
|
||||
IObjectWithKey,
|
||||
ISelectionZoneProps,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
SelectionZone
|
||||
} from "office-ui-fabric-react/lib/utilities/selection/index";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
|
||||
export interface QueriesGridComponentProps {
|
||||
queriesClient: QueriesClient;
|
||||
onQuerySelect: (query: DataModels.Query) => void;
|
||||
containerVisible: boolean;
|
||||
saveQueryEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface QueriesGridComponentState {
|
||||
queries: Query[];
|
||||
filteredResults: Query[];
|
||||
}
|
||||
|
||||
interface Query extends DataModels.Query, IObjectWithKey {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class QueriesGridComponent extends React.Component<QueriesGridComponentProps, QueriesGridComponentState> {
|
||||
private selection: Selection;
|
||||
private queryFilter: ITextField;
|
||||
|
||||
constructor(props: QueriesGridComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
queries: [],
|
||||
filteredResults: []
|
||||
};
|
||||
this.selection = new Selection();
|
||||
this.selection.setItems(this.state.filteredResults);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void {
|
||||
this.selection.setItems(
|
||||
this.state.filteredResults,
|
||||
!_.isEqual(prevState.filteredResults, this.state.filteredResults)
|
||||
);
|
||||
this.queryFilter && this.queryFilter.focus();
|
||||
const querySetupCompleted: boolean = !prevProps.saveQueryEnabled && this.props.saveQueryEnabled;
|
||||
const noQueryFiltersApplied: boolean = !this.queryFilter || !this.queryFilter.value;
|
||||
if (!this.props.containerVisible || !this.props.saveQueryEnabled) {
|
||||
return;
|
||||
} else if (noQueryFiltersApplied && (!prevProps.containerVisible || querySetupCompleted)) {
|
||||
// refresh only when pane is opened or query setup was recently completed
|
||||
this.fetchSavedQueries();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.queries.length === 0) {
|
||||
return this.renderBannerComponent();
|
||||
}
|
||||
return this.renderQueryGridComponent();
|
||||
}
|
||||
|
||||
private renderQueryGridComponent(): JSX.Element {
|
||||
const searchFilterProps: ITextFieldProps = {
|
||||
placeholder: "Search for Queries",
|
||||
ariaLabel: "Query filter input",
|
||||
onChange: this.onFilterInputChange,
|
||||
componentRef: (queryInput: ITextField) => (this.queryFilter = queryInput),
|
||||
styles: {
|
||||
root: { paddingBottom: "12px" },
|
||||
field: { fontSize: `${StyleConstants.mediumFontSize}px` }
|
||||
}
|
||||
};
|
||||
const selectionContainerProps: ISelectionZoneProps = {
|
||||
selection: this.selection,
|
||||
selectionMode: SelectionMode.single,
|
||||
onItemInvoked: (item: Query) => this.props.onQuerySelect(item)
|
||||
};
|
||||
const detailsListProps: IDetailsListProps = {
|
||||
items: this.state.filteredResults,
|
||||
columns: this.getColumns(),
|
||||
isHeaderVisible: false,
|
||||
setKey: "queryName",
|
||||
layoutMode: DetailsListLayoutMode.fixedColumns,
|
||||
selection: this.selection,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
onRenderRow: this.onRenderRow,
|
||||
styles: {
|
||||
root: { width: "100%" }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FocusZone style={{ width: "100%" }}>
|
||||
<TextField {...searchFilterProps} />
|
||||
<SelectionZone {...selectionContainerProps}>
|
||||
<DetailsList {...detailsListProps} />
|
||||
</SelectionZone>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBannerComponent(): JSX.Element {
|
||||
const bannerProps: React.ImgHTMLAttributes<HTMLImageElement> = {
|
||||
src: SaveQueryBannerIcon,
|
||||
alt: "Save query helper banner",
|
||||
style: {
|
||||
height: "150px",
|
||||
width: "310px",
|
||||
marginTop: "20px",
|
||||
border: `1px solid ${StyleConstants.BaseMedium}`
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
You have not saved any queries yet. <br /> <br />
|
||||
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save
|
||||
Query and follow the prompt in order to save the query.
|
||||
</div>
|
||||
<img {...bannerProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onFilterInputChange = (event: React.FormEvent<HTMLInputElement>, query: string): void => {
|
||||
if (query) {
|
||||
const filteredQueries: Query[] = this.state.queries.filter(
|
||||
(savedQuery: Query) =>
|
||||
savedQuery.queryName.indexOf(query) > -1 || savedQuery.queryName.toLowerCase().indexOf(query) > -1
|
||||
);
|
||||
this.setState({
|
||||
filteredResults: filteredQueries
|
||||
});
|
||||
} else {
|
||||
// no filter
|
||||
this.setState({
|
||||
filteredResults: this.state.queries
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
props.styles = {
|
||||
root: { width: "100%" },
|
||||
fields: {
|
||||
width: "100%",
|
||||
justifyContent: "space-between"
|
||||
},
|
||||
cell: {
|
||||
margin: "auto 0"
|
||||
}
|
||||
};
|
||||
return <DetailsRow data-selection-invoke={true} {...props} />;
|
||||
};
|
||||
|
||||
private getColumns(): IColumn[] {
|
||||
return [
|
||||
{
|
||||
key: "Name",
|
||||
name: "Name",
|
||||
fieldName: "queryName",
|
||||
minWidth: 260
|
||||
},
|
||||
{
|
||||
key: "Action",
|
||||
name: "Action",
|
||||
fieldName: null,
|
||||
minWidth: 70,
|
||||
onRender: (query: Query, index: number, column: IColumn) => {
|
||||
const buttonProps: IButtonProps = {
|
||||
iconProps: {
|
||||
iconName: "More",
|
||||
title: "More",
|
||||
ariaLabel: "More actions button"
|
||||
},
|
||||
menuIconProps: {
|
||||
styles: { root: { display: "none" } }
|
||||
},
|
||||
menuProps: {
|
||||
isBeakVisible: true,
|
||||
items: [
|
||||
{
|
||||
key: "Open",
|
||||
text: "Open query",
|
||||
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
|
||||
this.props.onQuerySelect(query);
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "Delete",
|
||||
text: "Delete query",
|
||||
onClick: async (
|
||||
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
|
||||
menuItem: any
|
||||
) => {
|
||||
if (window.confirm("Are you sure you want to delete this query?")) {
|
||||
const container = window.dataExplorer;
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title()
|
||||
});
|
||||
try {
|
||||
await this.props.queriesClient.deleteQuery(query);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title()
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error)
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
await this.fetchSavedQueries(); // get latest state
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
menuAs: (menuProps: IContextualMenuProps): JSX.Element => {
|
||||
return <ContextualMenu {...menuProps} />;
|
||||
}
|
||||
};
|
||||
return <IconButton {...buttonProps} />;
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private async fetchSavedQueries(): Promise<void> {
|
||||
let queries: Query[];
|
||||
try {
|
||||
queries = (await this.props.queriesClient.getQueries()) as Query[];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
queries = queries.map((query: Query) => {
|
||||
query.key = query.queryName;
|
||||
return query;
|
||||
});
|
||||
|
||||
// we do a deep equality check before setting the state to avoid infinite re-renders
|
||||
if (!_.isEqual(queries, this.state.queries)) {
|
||||
this.setState({
|
||||
filteredResults: queries,
|
||||
queries: queries
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
import * as _ from "underscore";
|
||||
import * as React from "react";
|
||||
import * as Constants from "../../../Common/Constants";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||
import {
|
||||
DetailsList,
|
||||
DetailsListLayoutMode,
|
||||
IDetailsListProps,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
} from "office-ui-fabric-react/lib/DetailsList";
|
||||
import { FocusZone } from "office-ui-fabric-react/lib/FocusZone";
|
||||
import { IconButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
|
||||
import { IColumn } from "office-ui-fabric-react/lib/DetailsList";
|
||||
import { IContextualMenuProps, ContextualMenu } from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
import {
|
||||
IObjectWithKey,
|
||||
ISelectionZoneProps,
|
||||
Selection,
|
||||
SelectionMode,
|
||||
SelectionZone,
|
||||
} from "office-ui-fabric-react/lib/utilities/selection/index";
|
||||
import { StyleConstants } from "../../../Common/Constants";
|
||||
import { TextField, ITextFieldProps, ITextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||
|
||||
import SaveQueryBannerIcon from "../../../../images/save_query_banner.png";
|
||||
import { QueriesClient } from "../../../Common/QueriesClient";
|
||||
import { getErrorMessage, getErrorStack } from "../../../Common/ErrorHandlingUtils";
|
||||
|
||||
export interface QueriesGridComponentProps {
|
||||
queriesClient: QueriesClient;
|
||||
onQuerySelect: (query: DataModels.Query) => void;
|
||||
containerVisible: boolean;
|
||||
saveQueryEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface QueriesGridComponentState {
|
||||
queries: Query[];
|
||||
filteredResults: Query[];
|
||||
}
|
||||
|
||||
interface Query extends DataModels.Query, IObjectWithKey {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class QueriesGridComponent extends React.Component<QueriesGridComponentProps, QueriesGridComponentState> {
|
||||
private selection: Selection;
|
||||
private queryFilter: ITextField;
|
||||
|
||||
constructor(props: QueriesGridComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
queries: [],
|
||||
filteredResults: [],
|
||||
};
|
||||
this.selection = new Selection();
|
||||
this.selection.setItems(this.state.filteredResults);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: QueriesGridComponentProps, prevState: QueriesGridComponentState): void {
|
||||
this.selection.setItems(
|
||||
this.state.filteredResults,
|
||||
!_.isEqual(prevState.filteredResults, this.state.filteredResults)
|
||||
);
|
||||
this.queryFilter && this.queryFilter.focus();
|
||||
const querySetupCompleted: boolean = !prevProps.saveQueryEnabled && this.props.saveQueryEnabled;
|
||||
const noQueryFiltersApplied: boolean = !this.queryFilter || !this.queryFilter.value;
|
||||
if (!this.props.containerVisible || !this.props.saveQueryEnabled) {
|
||||
return;
|
||||
} else if (noQueryFiltersApplied && (!prevProps.containerVisible || querySetupCompleted)) {
|
||||
// refresh only when pane is opened or query setup was recently completed
|
||||
this.fetchSavedQueries();
|
||||
}
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.queries.length === 0) {
|
||||
return this.renderBannerComponent();
|
||||
}
|
||||
return this.renderQueryGridComponent();
|
||||
}
|
||||
|
||||
private renderQueryGridComponent(): JSX.Element {
|
||||
const searchFilterProps: ITextFieldProps = {
|
||||
placeholder: "Search for Queries",
|
||||
ariaLabel: "Query filter input",
|
||||
onChange: this.onFilterInputChange,
|
||||
componentRef: (queryInput: ITextField) => (this.queryFilter = queryInput),
|
||||
styles: {
|
||||
root: { paddingBottom: "12px" },
|
||||
field: { fontSize: `${StyleConstants.mediumFontSize}px` },
|
||||
},
|
||||
};
|
||||
const selectionContainerProps: ISelectionZoneProps = {
|
||||
selection: this.selection,
|
||||
selectionMode: SelectionMode.single,
|
||||
onItemInvoked: (item: Query) => this.props.onQuerySelect(item),
|
||||
};
|
||||
const detailsListProps: IDetailsListProps = {
|
||||
items: this.state.filteredResults,
|
||||
columns: this.getColumns(),
|
||||
isHeaderVisible: false,
|
||||
setKey: "queryName",
|
||||
layoutMode: DetailsListLayoutMode.fixedColumns,
|
||||
selection: this.selection,
|
||||
selectionMode: SelectionMode.none,
|
||||
compact: true,
|
||||
onRenderRow: this.onRenderRow,
|
||||
styles: {
|
||||
root: { width: "100%" },
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<FocusZone style={{ width: "100%" }}>
|
||||
<TextField {...searchFilterProps} />
|
||||
<SelectionZone {...selectionContainerProps}>
|
||||
<DetailsList {...detailsListProps} />
|
||||
</SelectionZone>
|
||||
</FocusZone>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBannerComponent(): JSX.Element {
|
||||
const bannerProps: React.ImgHTMLAttributes<HTMLImageElement> = {
|
||||
src: SaveQueryBannerIcon,
|
||||
alt: "Save query helper banner",
|
||||
style: {
|
||||
height: "150px",
|
||||
width: "310px",
|
||||
marginTop: "20px",
|
||||
border: `1px solid ${StyleConstants.BaseMedium}`,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
You have not saved any queries yet. <br /> <br />
|
||||
To write a new query, open a new query tab and enter the desired query. Once ready to save, click on Save
|
||||
Query and follow the prompt in order to save the query.
|
||||
</div>
|
||||
<img {...bannerProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onFilterInputChange = (event: React.FormEvent<HTMLInputElement>, query: string): void => {
|
||||
if (query) {
|
||||
const filteredQueries: Query[] = this.state.queries.filter(
|
||||
(savedQuery: Query) =>
|
||||
savedQuery.queryName.indexOf(query) > -1 || savedQuery.queryName.toLowerCase().indexOf(query) > -1
|
||||
);
|
||||
this.setState({
|
||||
filteredResults: filteredQueries,
|
||||
});
|
||||
} else {
|
||||
// no filter
|
||||
this.setState({
|
||||
filteredResults: this.state.queries,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onRenderRow = (props: IDetailsRowProps): JSX.Element => {
|
||||
props.styles = {
|
||||
root: { width: "100%" },
|
||||
fields: {
|
||||
width: "100%",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
cell: {
|
||||
margin: "auto 0",
|
||||
},
|
||||
};
|
||||
return <DetailsRow data-selection-invoke={true} {...props} />;
|
||||
};
|
||||
|
||||
private getColumns(): IColumn[] {
|
||||
return [
|
||||
{
|
||||
key: "Name",
|
||||
name: "Name",
|
||||
fieldName: "queryName",
|
||||
minWidth: 260,
|
||||
},
|
||||
{
|
||||
key: "Action",
|
||||
name: "Action",
|
||||
fieldName: null,
|
||||
minWidth: 70,
|
||||
onRender: (query: Query, index: number, column: IColumn) => {
|
||||
const buttonProps: IButtonProps = {
|
||||
iconProps: {
|
||||
iconName: "More",
|
||||
title: "More",
|
||||
ariaLabel: "More actions button",
|
||||
},
|
||||
menuIconProps: {
|
||||
styles: { root: { display: "none" } },
|
||||
},
|
||||
menuProps: {
|
||||
isBeakVisible: true,
|
||||
items: [
|
||||
{
|
||||
key: "Open",
|
||||
text: "Open query",
|
||||
onClick: (event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>, menuItem: any) => {
|
||||
this.props.onQuerySelect(query);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Delete",
|
||||
text: "Delete query",
|
||||
onClick: async (
|
||||
event: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>,
|
||||
menuItem: any
|
||||
) => {
|
||||
if (window.confirm("Are you sure you want to delete this query?")) {
|
||||
const container = window.dataExplorer;
|
||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteSavedQuery, {
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title(),
|
||||
});
|
||||
try {
|
||||
await this.props.queriesClient.deleteQuery(query);
|
||||
TelemetryProcessor.traceSuccess(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
} catch (error) {
|
||||
TelemetryProcessor.traceFailure(
|
||||
Action.DeleteSavedQuery,
|
||||
{
|
||||
databaseAccountName: container && container.databaseAccount().name,
|
||||
defaultExperience: container && container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.ContextualPane,
|
||||
paneTitle: container && container.browseQueriesPane.title(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
}
|
||||
await this.fetchSavedQueries(); // get latest state
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
menuAs: (menuProps: IContextualMenuProps): JSX.Element => {
|
||||
return <ContextualMenu {...menuProps} />;
|
||||
},
|
||||
};
|
||||
return <IconButton {...buttonProps} />;
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private async fetchSavedQueries(): Promise<void> {
|
||||
let queries: Query[];
|
||||
try {
|
||||
queries = (await this.props.queriesClient.getQueries()) as Query[];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
queries = queries.map((query: Query) => {
|
||||
query.key = query.queryName;
|
||||
return query;
|
||||
});
|
||||
|
||||
// we do a deep equality check before setting the state to avoid infinite re-renders
|
||||
if (!_.isEqual(queries, this.state.queries)) {
|
||||
this.setState({
|
||||
filteredResults: queries,
|
||||
queries: queries,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
/**
|
||||
* This adapter is responsible to render the QueriesGrid React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export class QueriesGridComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private container: Explorer) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const props: QueriesGridComponentProps = {
|
||||
queriesClient: this.container.queriesClient,
|
||||
onQuerySelect: this.container.browseQueriesPane.loadSavedQuery,
|
||||
containerVisible: this.container.browseQueriesPane.visible(),
|
||||
saveQueryEnabled: this.container.canSaveQueries()
|
||||
};
|
||||
return <QueriesGridComponent {...props} />;
|
||||
}
|
||||
|
||||
public forceRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This adapter is responsible to render the QueriesGrid React component
|
||||
* If the component signals a change through the callback passed in the properties, it must render the React component when appropriate
|
||||
* and update any knockout observables passed from the parent.
|
||||
*/
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { QueriesGridComponent, QueriesGridComponentProps } from "./QueriesGridComponent";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import Explorer from "../../Explorer";
|
||||
|
||||
export class QueriesGridComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private container: Explorer) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
const props: QueriesGridComponentProps = {
|
||||
queriesClient: this.container.queriesClient,
|
||||
onQuerySelect: this.container.browseQueriesPane.loadSavedQuery,
|
||||
containerVisible: this.container.browseQueriesPane.visible(),
|
||||
saveQueryEnabled: this.container.canSaveQueries(),
|
||||
};
|
||||
return <QueriesGridComponent {...props} />;
|
||||
}
|
||||
|
||||
public forceRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ export class RadioSwitchComponent extends React.Component<RadioSwitchComponentPr
|
||||
tabIndex={0}
|
||||
key={choice.key}
|
||||
onClick={() => this.onSelect(choice)}
|
||||
onKeyPress={event => this.onKeyPress(event, choice)}
|
||||
onKeyPress={(event) => this.onKeyPress(event, choice)}
|
||||
>
|
||||
<Icon iconName={this.props.selectedKey === choice.key ? "RadioBtnOn" : "RadioBtnOff"} />
|
||||
<span className="caption">{choice.label}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@ import ko from "knockout";
|
||||
import { TtlType, isDirty } from "./SettingsUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
jest.mock("../../../Common/dataAccess/getIndexTransformationProgress", () => ({
|
||||
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined)
|
||||
getIndexTransformationProgress: jest.fn().mockReturnValue(undefined),
|
||||
}));
|
||||
import { updateCollection, updateMongoDBCollectionThroughRP } from "../../../Common/dataAccess/updateCollection";
|
||||
jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
@@ -20,20 +20,20 @@ jest.mock("../../../Common/dataAccess/updateCollection", () => ({
|
||||
conflictResolutionPolicy: undefined,
|
||||
changeFeedPolicy: undefined,
|
||||
analyticalStorageTtl: undefined,
|
||||
geospatialConfig: undefined
|
||||
geospatialConfig: undefined,
|
||||
} as DataModels.Collection),
|
||||
updateMongoDBCollectionThroughRP: jest.fn().mockReturnValue({
|
||||
id: undefined,
|
||||
shardKey: undefined,
|
||||
indexes: [],
|
||||
analyticalStorageTtl: undefined
|
||||
} as MongoDBCollectionResource)
|
||||
analyticalStorageTtl: undefined,
|
||||
} as MongoDBCollectionResource),
|
||||
}));
|
||||
import { updateOffer } from "../../../Common/dataAccess/updateOffer";
|
||||
import { MongoDBCollectionResource } from "../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import Q from "q";
|
||||
jest.mock("../../../Common/dataAccess/updateOffer", () => ({
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer)
|
||||
updateOffer: jest.fn().mockReturnValue({} as DataModels.Offer),
|
||||
}));
|
||||
|
||||
describe("SettingsComponent", () => {
|
||||
@@ -49,8 +49,8 @@ describe("SettingsComponent", () => {
|
||||
onUpdateTabsButtons: undefined,
|
||||
getPendingNotification: Q.Promise<DataModels.Notification>(() => {
|
||||
return;
|
||||
})
|
||||
})
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
@@ -71,7 +71,7 @@ describe("SettingsComponent", () => {
|
||||
isScaleSaveable: false,
|
||||
isScaleDiscardable: false,
|
||||
isSubSettingsSaveable: true,
|
||||
isSubSettingsDiscardable: true
|
||||
isSubSettingsDiscardable: true,
|
||||
});
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.isSaveSettingsButtonEnabled()).toEqual(true);
|
||||
@@ -93,7 +93,7 @@ describe("SettingsComponent", () => {
|
||||
manualThroughput: undefined,
|
||||
minimumThroughput: 400,
|
||||
id: "test",
|
||||
offerReplacePending: false
|
||||
offerReplacePending: false,
|
||||
});
|
||||
|
||||
const props = { ...baseProps };
|
||||
@@ -106,7 +106,7 @@ describe("SettingsComponent", () => {
|
||||
userCanChangeProvisioningTypes: true,
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: false,
|
||||
autoPilotThroughput: 1000
|
||||
autoPilotThroughput: 1000,
|
||||
});
|
||||
wrapper.update();
|
||||
expect(settingsComponentInstance.hasProvisioningTypeChanged()).toEqual(true);
|
||||
@@ -141,7 +141,7 @@ describe("SettingsComponent", () => {
|
||||
onDeleteDatabaseContextMenuClick: undefined,
|
||||
readSettings: undefined,
|
||||
onSettingsClick: undefined,
|
||||
loadOffer: undefined
|
||||
loadOffer: undefined,
|
||||
} as ViewModels.Database;
|
||||
newCollection.getDatabase = () => newDatabase;
|
||||
newCollection.offer = ko.observable(undefined);
|
||||
@@ -170,14 +170,14 @@ describe("SettingsComponent", () => {
|
||||
tableEndpoint: undefined,
|
||||
gremlinEndpoint: undefined,
|
||||
cassandraEndpoint: undefined,
|
||||
enableMultipleWriteLocations: true
|
||||
}
|
||||
enableMultipleWriteLocations: true,
|
||||
},
|
||||
});
|
||||
const newCollection = { ...collection };
|
||||
newCollection.container = newContainer;
|
||||
newCollection.conflictResolutionPolicy = ko.observable({
|
||||
mode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionProcedure: undefined
|
||||
conflictResolutionProcedure: undefined,
|
||||
} as DataModels.ConflictResolutionPolicy);
|
||||
|
||||
const props = { ...baseProps };
|
||||
@@ -193,7 +193,7 @@ describe("SettingsComponent", () => {
|
||||
wrapper.update();
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
settingsComponentInstance.mongoDBCollectionResource = {
|
||||
id: "id"
|
||||
id: "id",
|
||||
};
|
||||
await settingsComponentInstance.onSaveClick();
|
||||
expect(updateCollection).toBeCalled();
|
||||
@@ -238,7 +238,7 @@ describe("SettingsComponent", () => {
|
||||
|
||||
wrapper.setState({
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
conflictResolutionPolicyPath: conflictResolutionPolicyPath
|
||||
conflictResolutionPolicyPath: conflictResolutionPolicyPath,
|
||||
});
|
||||
wrapper.update();
|
||||
const settingsComponentInstance = wrapper.instance() as SettingsComponent;
|
||||
@@ -248,7 +248,7 @@ describe("SettingsComponent", () => {
|
||||
|
||||
wrapper.setState({
|
||||
conflictResolutionPolicyMode: DataModels.ConflictResolutionMode.Custom,
|
||||
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure
|
||||
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure,
|
||||
});
|
||||
wrapper.update();
|
||||
conflictResolutionPolicy = settingsComponentInstance.getUpdatedConflictResolutionPolicy();
|
||||
|
||||
@@ -16,7 +16,7 @@ import { mongoIndexingPolicyAADError } from "./SettingsRenderUtils";
|
||||
import { ScaleComponent, ScaleComponentProps } from "./SettingsSubComponents/ScaleComponent";
|
||||
import {
|
||||
MongoIndexingPolicyComponent,
|
||||
MongoIndexingPolicyComponentProps
|
||||
MongoIndexingPolicyComponentProps,
|
||||
} from "./SettingsSubComponents/MongoIndexingPolicy/MongoIndexingPolicyComponent";
|
||||
import {
|
||||
hasDatabaseSharedThroughput,
|
||||
@@ -30,11 +30,11 @@ import {
|
||||
MongoIndexTypes,
|
||||
parseConflictResolutionMode,
|
||||
parseConflictResolutionProcedure,
|
||||
getMongoNotification
|
||||
getMongoNotification,
|
||||
} from "./SettingsUtils";
|
||||
import {
|
||||
ConflictResolutionComponent,
|
||||
ConflictResolutionComponentProps
|
||||
ConflictResolutionComponentProps,
|
||||
} from "./SettingsSubComponents/ConflictResolutionComponent";
|
||||
import { SubSettingsComponent, SubSettingsComponentProps } from "./SettingsSubComponents/SubSettingsComponent";
|
||||
import { Pivot, PivotItem, IPivotProps, IPivotItemProps } from "office-ui-fabric-react";
|
||||
@@ -187,21 +187,21 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isConflictResolutionDirty: false,
|
||||
|
||||
initialNotification: undefined,
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab
|
||||
selectedTab: SettingsV2TabTypes.ScaleTab,
|
||||
};
|
||||
|
||||
this.saveSettingsButton = {
|
||||
isEnabled: this.isSaveSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
this.discardSettingsChangesButton = {
|
||||
isEnabled: this.isDiscardSettingsButtonEnabled,
|
||||
isVisible: () => {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
if (this.mongoDBCollectionResource) {
|
||||
this.setState({
|
||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
|
||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -277,7 +277,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isAutoPilotSelected: true,
|
||||
wasAutopilotOriginallySet: true,
|
||||
autoPilotThroughput: autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput
|
||||
autoPilotThroughputBaseline: autoscaleMaxThroughput,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -306,7 +306,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
databaseAccountName: this.container.databaseAccount()?.name,
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
});
|
||||
|
||||
const newCollection: DataModels.Collection = { ...this.collection.rawDataModel };
|
||||
@@ -339,14 +339,14 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
newCollection.changeFeedPolicy =
|
||||
this.changeFeedPolicyVisible && this.state.changeFeedPolicy === ChangeFeedPolicyState.On
|
||||
? {
|
||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration
|
||||
retentionDuration: Constants.BackendDefaults.maxChangeFeedRetentionDuration,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
newCollection.analyticalStorageTtl = this.getAnalyticalStorageTtl();
|
||||
|
||||
newCollection.geospatialConfig = {
|
||||
type: this.state.geospatialConfigType
|
||||
type: this.state.geospatialConfigType,
|
||||
};
|
||||
|
||||
const conflictResolutionChanges: DataModels.ConflictResolutionPolicy = this.getUpdatedConflictResolutionPolicy();
|
||||
@@ -376,7 +376,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isSubSettingsSaveable: false,
|
||||
isSubSettingsDiscardable: false,
|
||||
isIndexingPolicyDirty: false,
|
||||
isConflictResolutionDirty: false
|
||||
isConflictResolutionDirty: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const newMongoIndexes = this.getMongoIndexesToSave();
|
||||
const newMongoCollection: MongoDBCollectionResource = {
|
||||
...this.mongoDBCollectionResource,
|
||||
indexes: newMongoIndexes
|
||||
indexes: newMongoIndexes,
|
||||
};
|
||||
|
||||
this.mongoDBCollectionResource = await updateMongoDBCollectionThroughRP(
|
||||
@@ -399,7 +399,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
indexesToDrop: [],
|
||||
indexesToAdd: [],
|
||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes]
|
||||
currentMongoIndexes: [...this.mongoDBCollectionResource.indexes],
|
||||
});
|
||||
traceSuccess(
|
||||
Action.MongoIndexUpdated,
|
||||
@@ -409,7 +409,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
collectionName: this.collection?.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
@@ -424,7 +424,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error)
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
@@ -438,7 +438,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
collectionId: this.collection.id(),
|
||||
currentOffer: this.collection.offer(),
|
||||
autopilotThroughput: this.state.isAutoPilotSelected ? this.state.autoPilotThroughput : undefined,
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput
|
||||
manualThroughput: this.state.isAutoPilotSelected ? undefined : this.state.throughput,
|
||||
};
|
||||
if (this.hasProvisioningTypeChanged()) {
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
@@ -453,12 +453,12 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.state.isAutoPilotSelected) {
|
||||
this.setState({
|
||||
autoPilotThroughput: updatedOffer.autoscaleMaxThroughput,
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput
|
||||
autoPilotThroughputBaseline: updatedOffer.autoscaleMaxThroughput,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
throughput: updatedOffer.manualThroughput,
|
||||
throughputBaseline: updatedOffer.manualThroughput
|
||||
throughputBaseline: updatedOffer.manualThroughput,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -473,7 +473,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
collectionName: this.collection?.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
@@ -491,7 +491,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
error: getErrorMessage(error),
|
||||
errorStack: getErrorStack(error)
|
||||
errorStack: getErrorStack(error),
|
||||
},
|
||||
startKey
|
||||
);
|
||||
@@ -501,7 +501,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
|
||||
public onRevertClick = (): void => {
|
||||
trace(Action.SettingsV2Discarded, ActionModifiers.Mark, {
|
||||
message: "Settings Discarded"
|
||||
message: "Settings Discarded",
|
||||
});
|
||||
|
||||
this.setState({
|
||||
@@ -528,7 +528,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
isIndexingPolicyDirty: false,
|
||||
isMongoIndexingPolicySaveable: false,
|
||||
isMongoIndexingPolicyDiscardable: false,
|
||||
isConflictResolutionDirty: false
|
||||
isConflictResolutionDirty: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -564,7 +564,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
collectionName: this.collection.id(),
|
||||
defaultExperience: this.container.defaultExperience(),
|
||||
dataExplorerArea: Constants.Areas.Tab,
|
||||
tabTitle: this.props.settingsTab.tabTitle()
|
||||
tabTitle: this.props.settingsTab.tabTitle(),
|
||||
},
|
||||
this.props.settingsTab.onLoadStartKey
|
||||
);
|
||||
@@ -592,7 +592,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
const newMongoIndexWithType: AddMongoIndexProps = {
|
||||
mongoIndex: { key: { keys: [description] } } as MongoIndex,
|
||||
type: type,
|
||||
notification: notification
|
||||
notification: notification,
|
||||
};
|
||||
if (index === newIndexesToAdd.length) {
|
||||
newIndexesToAdd.push(newMongoIndexWithType);
|
||||
@@ -667,7 +667,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
}
|
||||
|
||||
const policy: DataModels.ConflictResolutionPolicy = {
|
||||
mode: parseConflictResolutionMode(this.state.conflictResolutionPolicyMode)
|
||||
mode: parseConflictResolutionMode(this.state.conflictResolutionPolicyMode),
|
||||
};
|
||||
|
||||
if (
|
||||
@@ -764,7 +764,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedure: conflictResolutionPolicyProcedure,
|
||||
conflictResolutionPolicyProcedureBaseline: conflictResolutionPolicyProcedure,
|
||||
geospatialConfigType: geoSpatialConfigType,
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType
|
||||
geospatialConfigTypeBaseline: geoSpatialConfigType,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -779,7 +779,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.saveSettingsButton.isEnabled()
|
||||
disabled: !this.saveSettingsButton.isEnabled(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -792,7 +792,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
commandButtonLabel: label,
|
||||
ariaLabel: label,
|
||||
hasPopup: false,
|
||||
disabled: !this.discardSettingsChangesButton.isEnabled()
|
||||
disabled: !this.discardSettingsChangesButton.isEnabled(),
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
@@ -827,7 +827,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
onMaxAutoPilotThroughputChange: this.onMaxAutoPilotThroughputChange,
|
||||
onScaleSaveableChange: this.onScaleSaveableChange,
|
||||
onScaleDiscardableChange: this.onScaleDiscardableChange,
|
||||
initialNotification: this.props.settingsTab.pendingNotification()
|
||||
initialNotification: this.props.settingsTab.pendingNotification(),
|
||||
};
|
||||
|
||||
const subSettingsComponentProps: SubSettingsComponentProps = {
|
||||
@@ -854,7 +854,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
changeFeedPolicyBaseline: this.state.changeFeedPolicyBaseline,
|
||||
onChangeFeedPolicyChange: this.onChangeFeedPolicyChange,
|
||||
onSubSettingsSaveableChange: this.onSubSettingsSaveableChange,
|
||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange
|
||||
onSubSettingsDiscardableChange: this.onSubSettingsDiscardableChange,
|
||||
};
|
||||
|
||||
const indexingPolicyComponentProps: IndexingPolicyComponentProps = {
|
||||
@@ -866,7 +866,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
logIndexingPolicySuccessMessage: this.logIndexingPolicySuccessMessage,
|
||||
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange
|
||||
onIndexingPolicyDirtyChange: this.onIndexingPolicyDirtyChange,
|
||||
};
|
||||
|
||||
const mongoIndexingPolicyComponentProps: MongoIndexingPolicyComponentProps = {
|
||||
@@ -880,7 +880,7 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
indexTransformationProgress: this.state.indexTransformationProgress,
|
||||
refreshIndexTransformationProgress: this.refreshIndexTransformationProgress,
|
||||
onMongoIndexingPolicySaveableChange: this.onMongoIndexingPolicySaveableChange,
|
||||
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange
|
||||
onMongoIndexingPolicyDiscardableChange: this.onMongoIndexingPolicyDiscardableChange,
|
||||
};
|
||||
|
||||
const conflictResolutionPolicyComponentProps: ConflictResolutionComponentProps = {
|
||||
@@ -895,37 +895,37 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
conflictResolutionPolicyProcedure: this.state.conflictResolutionPolicyProcedure,
|
||||
conflictResolutionPolicyProcedureBaseline: this.state.conflictResolutionPolicyProcedureBaseline,
|
||||
onConflictResolutionPolicyProcedureChange: this.onConflictResolutionPolicyProcedureChange,
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange
|
||||
onConflictResolutionDirtyChange: this.onConflictResolutionDirtyChange,
|
||||
};
|
||||
|
||||
const tabs: SettingsV2TabInfo[] = [];
|
||||
if (!hasDatabaseSharedThroughput(this.collection) && this.collection.offer()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ScaleTab,
|
||||
content: <ScaleComponent {...scaleComponentProps} />
|
||||
content: <ScaleComponent {...scaleComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.SubSettingsTab,
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />
|
||||
content: <SubSettingsComponent {...subSettingsComponentProps} />,
|
||||
});
|
||||
|
||||
if (this.shouldShowIndexingPolicyEditor) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />
|
||||
content: <IndexingPolicyComponent {...indexingPolicyComponentProps} />,
|
||||
});
|
||||
} else if (this.container.isPreferredApiMongoDB()) {
|
||||
if (isEmpty(this.container.features())) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: mongoIndexingPolicyAADError
|
||||
content: mongoIndexingPolicyAADError,
|
||||
});
|
||||
} else if (this.container.isEnableMongoCapabilityPresent()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.IndexingPolicyTab,
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />
|
||||
content: <MongoIndexingPolicyComponent {...mongoIndexingPolicyComponentProps} />,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -933,20 +933,20 @@ export class SettingsComponent extends React.Component<SettingsComponentProps, S
|
||||
if (this.hasConflictResolution()) {
|
||||
tabs.push({
|
||||
tab: SettingsV2TabTypes.ConflictResolutionTab,
|
||||
content: <ConflictResolutionComponent {...conflictResolutionPolicyComponentProps} />
|
||||
content: <ConflictResolutionComponent {...conflictResolutionPolicyComponentProps} />,
|
||||
});
|
||||
}
|
||||
|
||||
const pivotProps: IPivotProps = {
|
||||
onLinkClick: this.onPivotChange,
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab]
|
||||
selectedKey: SettingsV2TabTypes[this.state.selectedTab],
|
||||
};
|
||||
|
||||
const pivotItems = tabs.map(tab => {
|
||||
const pivotItems = tabs.map((tab) => {
|
||||
const pivotItemProps: IPivotItemProps = {
|
||||
itemKey: SettingsV2TabTypes[tab.tab],
|
||||
style: { marginTop: 20 },
|
||||
headerText: getTabTitle(tab.tab)
|
||||
headerText: getTabTitle(tab.tab),
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
ManualEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown
|
||||
getRuPriceBreakdown,
|
||||
} from "./SettingsRenderUtils";
|
||||
|
||||
class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
@@ -31,15 +31,15 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
{ key: "costType", name: "", fieldName: "costType", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "hourly", name: "Hourly", fieldName: "hourly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "daily", name: "Daily", fieldName: "daily", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true }
|
||||
{ key: "monthly", name: "Monthly", fieldName: "monthly", minWidth: 100, maxWidth: 200, isResizable: true },
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
costType: <Text>Current Cost</Text>,
|
||||
hourly: <Text>$ 1.02</Text>,
|
||||
daily: <Text>$ 24.48</Text>,
|
||||
monthly: <Text>$ 744.6</Text>
|
||||
}
|
||||
monthly: <Text>$ 744.6</Text>,
|
||||
},
|
||||
];
|
||||
const priceBreakdown: PriceBreakdown = {
|
||||
hourlyPrice: 1.02,
|
||||
@@ -47,7 +47,7 @@ class SettingsRenderUtilsTestComponent extends React.Component {
|
||||
monthlyPrice: 744.6,
|
||||
pricePerRu: 0.00051,
|
||||
currency: "RMB",
|
||||
currencySign: "¥"
|
||||
currencySign: "¥",
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getMultimasterMultiplier,
|
||||
computeRUUsagePriceHourly,
|
||||
getPricePerRu,
|
||||
estimatedCostDisclaimer
|
||||
estimatedCostDisclaimer,
|
||||
} from "../../../Utils/PricingUtils";
|
||||
import {
|
||||
ITextFieldStyles,
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
DetailsListLayoutMode,
|
||||
IDetailsRowProps,
|
||||
DetailsRow,
|
||||
IDetailsColumnStyles
|
||||
IDetailsColumnStyles,
|
||||
} from "office-ui-fabric-react";
|
||||
import { isDirtyTypes, isDirty } from "./SettingsUtils";
|
||||
|
||||
@@ -71,49 +71,49 @@ export const infoAndToolTipTextStyle: ITextStyles = { root: { fontSize: 14 } };
|
||||
export const noLeftPaddingCheckBoxStyle: ICheckboxStyles = {
|
||||
label: {
|
||||
margin: 0,
|
||||
padding: "2 0 2 0"
|
||||
padding: "2 0 2 0",
|
||||
},
|
||||
text: {
|
||||
fontSize: 12
|
||||
}
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
||||
|
||||
export const subComponentStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 20 }
|
||||
tokens: { childrenGap: 20 },
|
||||
};
|
||||
|
||||
export const titleAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 5 }
|
||||
tokens: { childrenGap: 5 },
|
||||
};
|
||||
|
||||
export const mongoWarningStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 5 }
|
||||
tokens: { childrenGap: 5 },
|
||||
};
|
||||
|
||||
export const mongoErrorMessageStyles: Partial<IMessageBarStyles> = { root: { marginLeft: 10 } };
|
||||
|
||||
export const createAndAddMongoIndexStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 5 }
|
||||
tokens: { childrenGap: 5 },
|
||||
};
|
||||
|
||||
export const addMongoIndexStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 }
|
||||
tokens: { childrenGap: 10 },
|
||||
};
|
||||
|
||||
export const checkBoxAndInputStackProps: Partial<IStackProps> = {
|
||||
tokens: { childrenGap: 10 }
|
||||
tokens: { childrenGap: 10 },
|
||||
};
|
||||
|
||||
export const toolTipLabelStackTokens: IStackTokens = {
|
||||
childrenGap: 6
|
||||
childrenGap: 6,
|
||||
};
|
||||
|
||||
export const accordionStackTokens: IStackTokens = {
|
||||
childrenGap: 10
|
||||
childrenGap: 10,
|
||||
};
|
||||
|
||||
export const addMongoIndexSubElementsTokens: IStackTokens = {
|
||||
childrenGap: 20
|
||||
childrenGap: 20,
|
||||
};
|
||||
|
||||
export const accordionIconStyles: IIconStyles = { root: { paddingTop: 7 } };
|
||||
@@ -128,30 +128,30 @@ export const transparentDetailsRowStyles: Partial<IDetailsRowStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
":hover": {
|
||||
background: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
background: "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const transparentDetailsHeaderStyle: Partial<IDetailsColumnStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
":hover": {
|
||||
background: "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
background: "transparent",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const customDetailsListStyles: Partial<IDetailsListStyles> = {
|
||||
root: {
|
||||
selectors: {
|
||||
".ms-FocusZone": {
|
||||
paddingTop: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
paddingTop: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
@@ -159,16 +159,16 @@ export const separatorStyles: Partial<ISeparatorStyles> = {
|
||||
{
|
||||
selectors: {
|
||||
"::before": {
|
||||
background: StyleConstants.BaseMedium
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
background: StyleConstants.BaseMedium,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const messageBarStyles: Partial<IMessageBarStyles> = {
|
||||
root: { marginTop: "5px", backgroundColor: "white" },
|
||||
text: { fontSize: 14 }
|
||||
text: { fontSize: 14 },
|
||||
};
|
||||
|
||||
export const throughputUnit = "RU/s";
|
||||
@@ -224,7 +224,7 @@ export const getRuPriceBreakdown = (
|
||||
requestUnits: throughput,
|
||||
numberOfRegions: numberOfRegions,
|
||||
multimasterEnabled: isMultimaster,
|
||||
isAutoscale: isAutoscale
|
||||
isAutoscale: isAutoscale,
|
||||
});
|
||||
const basePricePerRu: number = isAutoscale
|
||||
? getAutoscalePricePerRu(serverId, getMultimasterMultiplier(numberOfRegions, isMultimaster))
|
||||
@@ -235,7 +235,7 @@ export const getRuPriceBreakdown = (
|
||||
monthlyPrice: hourlyPrice * hoursInAMonth,
|
||||
pricePerRu: basePricePerRu * getMultimasterMultiplier(numberOfRegions, isMultimaster),
|
||||
currency: getPriceCurrency(serverId),
|
||||
currencySign: getCurrencySign(serverId)
|
||||
currencySign: getCurrencySign(serverId),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -488,10 +488,10 @@ export const getTextFieldStyles = (current: isDirtyTypes, baseline: isDirtyTypes
|
||||
selectors: {
|
||||
":disabled": {
|
||||
backgroundColor: StyleConstants.BaseMedium,
|
||||
borderColor: StyleConstants.BaseMediumHigh
|
||||
}
|
||||
}
|
||||
}
|
||||
borderColor: StyleConstants.BaseMediumHigh,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTypes): Partial<IChoiceGroupStyles> => ({
|
||||
@@ -499,18 +499,18 @@ export const getChoiceGroupStyles = (current: isDirtyTypes, baseline: isDirtyTyp
|
||||
{
|
||||
selectors: {
|
||||
".ms-ChoiceField-field.is-checked::before": {
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
|
||||
},
|
||||
".ms-ChoiceField-field.is-checked::after": {
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : ""
|
||||
borderColor: isDirty(current, baseline) ? StyleConstants.Dirty : "",
|
||||
},
|
||||
".ms-ChoiceField-wrapper label": {
|
||||
whiteSpace: "nowrap",
|
||||
fontSize: 14,
|
||||
fontFamily: StyleConstants.DataExplorerFont,
|
||||
padding: "2px 5px"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
padding: "2px 5px",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("ConflictResolutionComponent", () => {
|
||||
},
|
||||
onConflictResolutionDirtyChange: () => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
it("Sproc text field displayed", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
conflictResolutionLwwTooltip,
|
||||
conflictResolutionCustomToolTip,
|
||||
subComponentStackProps,
|
||||
getChoiceGroupStyles
|
||||
getChoiceGroupStyles,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { TextField, ITextFieldProps, Stack, IChoiceGroupOption, ChoiceGroup } from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
@@ -35,9 +35,9 @@ export class ConflictResolutionComponent extends React.Component<ConflictResolut
|
||||
private conflictResolutionChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{
|
||||
key: DataModels.ConflictResolutionMode.LastWriterWins,
|
||||
text: "Last Write Wins (default)"
|
||||
text: "Last Write Wins (default)",
|
||||
},
|
||||
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" }
|
||||
{ key: DataModels.ConflictResolutionMode.Custom, text: "Merge Procedure (custom)" },
|
||||
];
|
||||
|
||||
componentDidMount(): void {
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("IndexingPolicyComponent", () => {
|
||||
automatic: false,
|
||||
indexingMode: "",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
excludedPaths: [],
|
||||
};
|
||||
const baseProps: IndexingPolicyComponentProps = {
|
||||
shouldDiscardIndexingPolicy: false,
|
||||
@@ -27,7 +27,7 @@ describe("IndexingPolicyComponent", () => {
|
||||
return;
|
||||
},
|
||||
indexTransformationProgress: undefined,
|
||||
refreshIndexTransformationProgress: () => new Promise(jest.fn())
|
||||
refreshIndexTransformationProgress: () => new Promise(jest.fn()),
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
constructor(props: IndexingPolicyComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
indexingPolicyContentIsValid: true
|
||||
indexingPolicyContentIsValid: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
this.createIndexingPolicyEditor();
|
||||
} else {
|
||||
this.indexingPolicyEditor.updateOptions({
|
||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress)
|
||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||
});
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
const value: string = JSON.stringify(this.props.indexingPolicyContent, undefined, 4);
|
||||
@@ -91,7 +91,7 @@ export class IndexingPolicyComponent extends React.Component<
|
||||
value: value,
|
||||
language: "json",
|
||||
readOnly: isIndexTransforming(this.props.indexTransformationProgress),
|
||||
ariaLabel: "Indexing Policy"
|
||||
ariaLabel: "Indexing Policy",
|
||||
});
|
||||
if (this.indexingPolicyEditor) {
|
||||
const indexingPolicyEditorModel = this.indexingPolicyEditor.getModel();
|
||||
|
||||
@@ -6,7 +6,7 @@ describe("IndexingPolicyRefreshComponent", () => {
|
||||
it("renders", () => {
|
||||
const props: IndexingPolicyRefreshComponentProps = {
|
||||
indexTransformationProgress: 90,
|
||||
refreshIndexTransformationProgress: () => new Promise(jest.fn())
|
||||
refreshIndexTransformationProgress: () => new Promise(jest.fn()),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<IndexingPolicyRefreshComponent {...props} />);
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { MessageBar, MessageBarType } from "office-ui-fabric-react";
|
||||
import {
|
||||
mongoIndexTransformationRefreshingMessage,
|
||||
renderMongoIndexTransformationRefreshMessage
|
||||
renderMongoIndexTransformationRefreshMessage,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { handleError } from "../../../../../Common/ErrorHandlingUtils";
|
||||
import { isIndexTransforming } from "../../SettingsUtils";
|
||||
@@ -23,7 +23,7 @@ export class IndexingPolicyRefreshComponent extends React.Component<
|
||||
constructor(props: IndexingPolicyRefreshComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isRefreshing: false
|
||||
isRefreshing: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ describe("AddMongoIndexComponent", () => {
|
||||
},
|
||||
onDiscard: () => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = shallow(<AddMongoIndexComponent {...props} />);
|
||||
|
||||
@@ -7,21 +7,21 @@ import {
|
||||
TextField,
|
||||
Dropdown,
|
||||
IDropdownOption,
|
||||
ITextField
|
||||
ITextField,
|
||||
} from "office-ui-fabric-react";
|
||||
import {
|
||||
addMongoIndexSubElementsTokens,
|
||||
mongoErrorMessageStyles,
|
||||
mongoWarningStackProps,
|
||||
shortWidthDropDownStyles,
|
||||
shortWidthTextFieldStyles
|
||||
shortWidthTextFieldStyles,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
getMongoIndexTypeText,
|
||||
MongoIndexTypes,
|
||||
MongoNotificationMessage,
|
||||
MongoNotificationType,
|
||||
MongoWildcardPlaceHolder
|
||||
MongoWildcardPlaceHolder,
|
||||
} from "../../SettingsUtils";
|
||||
|
||||
export interface AddMongoIndexComponentProps {
|
||||
@@ -39,7 +39,7 @@ export class AddMongoIndexComponent extends React.Component<AddMongoIndexCompone
|
||||
private indexTypes: IDropdownOption[] = [MongoIndexTypes.Single, MongoIndexTypes.Wildcard].map(
|
||||
(value: MongoIndexTypes) => ({
|
||||
text: getMongoIndexTypeText(value),
|
||||
key: value
|
||||
key: value,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("MongoIndexingPolicyComponent", () => {
|
||||
},
|
||||
onMongoIndexingPolicyDiscardableChange: () => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
@@ -55,24 +55,24 @@ describe("MongoIndexingPolicyComponent", () => {
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
sampleWarning
|
||||
sampleWarning,
|
||||
],
|
||||
[
|
||||
{ type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
undefined
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
{ type: MongoNotificationType.Error, message: sampleError } as MongoNotificationMessage,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
undefined
|
||||
undefined,
|
||||
],
|
||||
[undefined, false, true, true, undefined],
|
||||
[undefined, true, true, true, undefined]
|
||||
[undefined, true, true, true, undefined],
|
||||
];
|
||||
|
||||
test.each(cases)(
|
||||
@@ -87,7 +87,7 @@ describe("MongoIndexingPolicyComponent", () => {
|
||||
const addMongoIndexProps = {
|
||||
mongoIndex: { key: { keys: ["sampleKey"] } },
|
||||
type: MongoIndexTypes.Single,
|
||||
notification: notification
|
||||
notification: notification,
|
||||
};
|
||||
|
||||
let indexesToDrop: number[] = [];
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
MessageBarType,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
Separator
|
||||
Separator,
|
||||
} from "office-ui-fabric-react";
|
||||
import {
|
||||
addMongoIndexStackProps,
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
separatorStyles,
|
||||
indexingPolicynUnsavedWarningMessage,
|
||||
infoAndToolTipTextStyle,
|
||||
onRenderRow
|
||||
onRenderRow,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import { MongoIndex } from "../../../../../Utils/arm/generatedClients/2020-04-01/types";
|
||||
import {
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
MongoNotificationType,
|
||||
getMongoIndexType,
|
||||
getMongoIndexTypeText,
|
||||
isIndexTransforming
|
||||
isIndexTransforming,
|
||||
} from "../../SettingsUtils";
|
||||
import { AddMongoIndexComponent } from "./AddMongoIndexComponent";
|
||||
import { CollapsibleSectionComponent } from "../../../CollapsiblePanel/CollapsibleSectionComponent";
|
||||
@@ -71,8 +71,8 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true
|
||||
}
|
||||
isResizable: true,
|
||||
},
|
||||
];
|
||||
|
||||
private indexesToBeDroppedColumns: IColumn[] = [
|
||||
@@ -84,8 +84,8 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
fieldName: "actionButton",
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true
|
||||
}
|
||||
isResizable: true,
|
||||
},
|
||||
];
|
||||
|
||||
componentDidUpdate(prevProps: MongoIndexingPolicyComponentProps): void {
|
||||
@@ -114,7 +114,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
return false;
|
||||
}
|
||||
|
||||
const addErrorsExist = !!this.props.indexesToAdd.find(addMongoIndexProps => addMongoIndexProps.notification);
|
||||
const addErrorsExist = !!this.props.indexesToAdd.find((addMongoIndexProps) => addMongoIndexProps.notification);
|
||||
|
||||
if (addErrorsExist) {
|
||||
return false;
|
||||
@@ -129,7 +129,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
|
||||
public getMongoWarningNotificationMessage = (): JSX.Element => {
|
||||
const warningMessage = this.props.indexesToAdd.find(
|
||||
addMongoIndexProps => addMongoIndexProps.notification?.type === MongoNotificationType.Warning
|
||||
(addMongoIndexProps) => addMongoIndexProps.notification?.type === MongoNotificationType.Warning
|
||||
)?.notification.message;
|
||||
|
||||
if (warningMessage) {
|
||||
@@ -172,7 +172,7 @@ export class MongoIndexingPolicyComponent extends React.Component<MongoIndexingP
|
||||
mongoIndexDisplayProps = {
|
||||
definition: <Text>{definition}</Text>,
|
||||
type: <Text>{getMongoIndexTypeText(type)}</Text>,
|
||||
actionButton: definition === MongoIndexIdField ? <></> : this.getActionButton(arrayPosition, isCurrentIndex)
|
||||
actionButton: definition === MongoIndexIdField ? <></> : this.getActionButton(arrayPosition, isCurrentIndex),
|
||||
};
|
||||
}
|
||||
return mongoIndexDisplayProps;
|
||||
|
||||
@@ -40,8 +40,8 @@ describe("ScaleComponent", () => {
|
||||
return;
|
||||
},
|
||||
initialNotification: {
|
||||
description: `Throughput update for ${targetThroughput} ${throughputUnit}`
|
||||
} as DataModels.Notification
|
||||
description: `Throughput update for ${targetThroughput} ${throughputUnit}`,
|
||||
} as DataModels.Notification,
|
||||
};
|
||||
|
||||
it("renders with correct initial notification", () => {
|
||||
@@ -59,12 +59,12 @@ describe("ScaleComponent", () => {
|
||||
autoscaleMaxThroughput: maxThroughput,
|
||||
minimumThroughput: 400,
|
||||
id: "offer",
|
||||
offerReplacePending: true
|
||||
offerReplacePending: true,
|
||||
});
|
||||
const newProps = {
|
||||
...baseProps,
|
||||
initialNotification: undefined as DataModels.Notification,
|
||||
collection: newCollection
|
||||
collection: newCollection,
|
||||
};
|
||||
wrapper = shallow(<ScaleComponent {...newProps} />);
|
||||
expect(wrapper.exists("#throughputApplyShortDelayMessage")).toEqual(true);
|
||||
@@ -95,10 +95,10 @@ describe("ScaleComponent", () => {
|
||||
capabilities: [
|
||||
{
|
||||
name: Constants.CapabilityNames.EnableAutoScale.toLowerCase(),
|
||||
description: undefined
|
||||
}
|
||||
]
|
||||
}
|
||||
description: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const props = { ...baseProps, container: newContainer };
|
||||
const scaleComponent = new ScaleComponent(props);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
throughputUnit,
|
||||
getThroughputApplyLongDelayMessage,
|
||||
getThroughputApplyShortDelayMessage,
|
||||
updateThroughputBeyondLimitWarningMessage
|
||||
updateThroughputBeyondLimitWarningMessage,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { hasDatabaseSharedThroughput } from "../SettingsUtils";
|
||||
import * as AutoPilotUtils from "../../../../Utils/AutoPilotUtils";
|
||||
|
||||
@@ -54,7 +54,7 @@ describe("SubSettingsComponent", () => {
|
||||
},
|
||||
onSubSettingsDiscardableChange: () => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
TtlOn,
|
||||
TtlOff,
|
||||
TtlOnNoDefault,
|
||||
getSanitizedInputValue
|
||||
getSanitizedInputValue,
|
||||
} from "../SettingsUtils";
|
||||
import Explorer from "../../../Explorer";
|
||||
import { Int32 } from "../../../Panes/Tables/Validators/EntityPropertyValidationCommon";
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
titleAndInputStackProps,
|
||||
getChoiceGroupStyles,
|
||||
ttlWarning,
|
||||
messageBarStyles
|
||||
messageBarStyles,
|
||||
} from "../SettingsRenderUtils";
|
||||
import { ToolTipLabelComponent } from "./ToolTipLabelComponent";
|
||||
|
||||
@@ -119,7 +119,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
private ttlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off" },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" }
|
||||
{ key: TtlType.On, text: "On" },
|
||||
];
|
||||
|
||||
public getTtlValue = (value: string): TtlType => {
|
||||
@@ -207,7 +207,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
private analyticalTtlChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: TtlType.Off, text: "Off", disabled: true },
|
||||
{ key: TtlType.OnNoDefault, text: "On (no default)" },
|
||||
{ key: TtlType.On, text: "On" }
|
||||
{ key: TtlType.On, text: "On" },
|
||||
];
|
||||
|
||||
private getAnalyticalStorageTtlComponent = (): JSX.Element => (
|
||||
@@ -244,7 +244,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
|
||||
private geoSpatialConfigTypeChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: GeospatialConfigType.Geography, text: "Geography" },
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry" }
|
||||
{ key: GeospatialConfigType.Geometry, text: "Geometry" },
|
||||
];
|
||||
|
||||
private getGeoSpatialComponent = (): JSX.Element => (
|
||||
@@ -260,7 +260,7 @@ export class SubSettingsComponent extends React.Component<SubSettingsComponentPr
|
||||
|
||||
private changeFeedChoiceGroupOptions: IChoiceGroupOption[] = [
|
||||
{ key: ChangeFeedPolicyState.Off, text: "Off" },
|
||||
{ key: ChangeFeedPolicyState.On, text: "On" }
|
||||
{ key: ChangeFeedPolicyState.On, text: "On" },
|
||||
];
|
||||
|
||||
private getChangeFeedComponent = (): JSX.Element => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { shallow } from "enzyme";
|
||||
import React from "react";
|
||||
import {
|
||||
ThroughputInputAutoPilotV3Component,
|
||||
ThroughputInputAutoPilotV3Props
|
||||
ThroughputInputAutoPilotV3Props,
|
||||
} from "./ThroughputInputAutoPilotV3Component";
|
||||
import * as DataModels from "../../../../../Contracts/DataModels";
|
||||
|
||||
@@ -43,7 +43,7 @@ describe("ThroughputInputAutoPilotV3Component", () => {
|
||||
onScaleDiscardableChange: () => {
|
||||
return;
|
||||
},
|
||||
getThroughputWarningMessage: () => undefined
|
||||
getThroughputWarningMessage: () => undefined,
|
||||
};
|
||||
|
||||
it("throughput input visible", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
AutoscaleEstimatedSpendingDisplayProps,
|
||||
PriceBreakdown,
|
||||
getRuPriceBreakdown,
|
||||
transparentDetailsHeaderStyle
|
||||
transparentDetailsHeaderStyle,
|
||||
} from "../../SettingsRenderUtils";
|
||||
import {
|
||||
Text,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
Link,
|
||||
MessageBar,
|
||||
FontIcon,
|
||||
IColumn
|
||||
IColumn,
|
||||
} from "office-ui-fabric-react";
|
||||
import { ToolTipLabelComponent } from "../ToolTipLabelComponent";
|
||||
import { getSanitizedInputValue, IsComponentDirtyResult, isDirty } from "../../SettingsUtils";
|
||||
@@ -95,7 +95,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
private autoPilotInputMaxValue: number;
|
||||
private options: IChoiceGroupOption[] = [
|
||||
{ key: "true", text: "Autoscale" },
|
||||
{ key: "false", text: "Manual" }
|
||||
{ key: "false", text: "Manual" },
|
||||
];
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -157,7 +157,7 @@ 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 > 400,
|
||||
};
|
||||
|
||||
this.step = this.props.step ?? ThroughputInputAutoPilotV3Component.defaultStep;
|
||||
@@ -224,7 +224,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "minPerMonth",
|
||||
@@ -233,7 +233,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "maxPerMonth",
|
||||
@@ -242,8 +242,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
}
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: AutoscaleEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
@@ -257,8 +257,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
@@ -288,7 +288,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "hourly",
|
||||
@@ -327,7 +327,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "daily",
|
||||
@@ -336,7 +336,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
{
|
||||
key: "monthly",
|
||||
@@ -345,8 +345,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
minWidth: 100,
|
||||
maxWidth: 200,
|
||||
isResizable: true,
|
||||
styles: transparentDetailsHeaderStyle
|
||||
}
|
||||
styles: transparentDetailsHeaderStyle,
|
||||
},
|
||||
];
|
||||
const estimatedSpendingItems: ManualEstimatedSpendingDisplayProps[] = [
|
||||
{
|
||||
@@ -365,8 +365,8 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
<Text>
|
||||
{prices.currencySign} {calculateEstimateNumber(prices.monthlyPrice)}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
if (newThroughput) {
|
||||
@@ -403,7 +403,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
{newPrices.currencySign} {calculateEstimateNumber(newPrices.monthlyPrice)}
|
||||
</b>
|
||||
</Text>
|
||||
)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ export class ThroughputInputAutoPilotV3Component extends React.Component<
|
||||
databaseName: this.props.databaseName,
|
||||
collectionName: this.props.collectionName,
|
||||
apiKind: userContext.defaultExperience,
|
||||
dataExplorerArea: "Scale Tab V2"
|
||||
dataExplorerArea: "Scale Tab V2",
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ToolTipLabelComponent, ToolTipLabelComponentProps } from "./ToolTipLabe
|
||||
describe("ToolTipLabelComponent", () => {
|
||||
const props: ToolTipLabelComponentProps = {
|
||||
label: "sample tool tip label",
|
||||
toolTipElement: <span>sample tool tip text</span>
|
||||
toolTipElement: <span>sample tool tip text</span>,
|
||||
};
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getMongoIndexTypeText,
|
||||
SingleFieldText,
|
||||
WildcardText,
|
||||
isIndexTransforming
|
||||
isIndexTransforming,
|
||||
} from "./SettingsUtils";
|
||||
import * as DataModels from "../../../Contracts/DataModels";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
@@ -45,7 +45,7 @@ describe("SettingsUtils", () => {
|
||||
onDeleteDatabaseContextMenuClick: undefined,
|
||||
readSettings: undefined,
|
||||
onSettingsClick: undefined,
|
||||
loadOffer: undefined
|
||||
loadOffer: undefined,
|
||||
} as ViewModels.Database;
|
||||
};
|
||||
newCollection.offer(undefined);
|
||||
@@ -67,7 +67,7 @@ describe("SettingsUtils", () => {
|
||||
automatic: true,
|
||||
indexingMode: "consistent",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
excludedPaths: [],
|
||||
} as DataModels.IndexingPolicy;
|
||||
|
||||
it("works on all types", () => {
|
||||
|
||||
@@ -15,23 +15,23 @@ export const WildcardText = "Wildcard";
|
||||
|
||||
export enum ChangeFeedPolicyState {
|
||||
Off = "Off",
|
||||
On = "On"
|
||||
On = "On",
|
||||
}
|
||||
|
||||
export enum TtlType {
|
||||
Off = "off",
|
||||
On = "on",
|
||||
OnNoDefault = "on-nodefault"
|
||||
OnNoDefault = "on-nodefault",
|
||||
}
|
||||
|
||||
export enum GeospatialConfigType {
|
||||
Geography = "Geography",
|
||||
Geometry = "Geometry"
|
||||
Geometry = "Geometry",
|
||||
}
|
||||
|
||||
export enum MongoIndexTypes {
|
||||
Single = "Single",
|
||||
Wildcard = "Wildcard"
|
||||
Wildcard = "Wildcard",
|
||||
}
|
||||
|
||||
export interface AddMongoIndexProps {
|
||||
@@ -44,7 +44,7 @@ export enum SettingsV2TabTypes {
|
||||
ScaleTab,
|
||||
ConflictResolutionTab,
|
||||
SubSettingsTab,
|
||||
IndexingPolicyTab
|
||||
IndexingPolicyTab,
|
||||
}
|
||||
|
||||
export interface IsComponentDirtyResult {
|
||||
@@ -54,7 +54,7 @@ export interface IsComponentDirtyResult {
|
||||
|
||||
export enum MongoNotificationType {
|
||||
Warning = "Warning",
|
||||
Error = "Error"
|
||||
Error = "Error",
|
||||
}
|
||||
|
||||
export interface MongoNotificationMessage {
|
||||
@@ -155,19 +155,19 @@ export const getMongoNotification = (description: string, type: MongoIndexTypes)
|
||||
if (description && !type) {
|
||||
return {
|
||||
type: MongoNotificationType.Warning,
|
||||
message: "Please select a type for each index."
|
||||
message: "Please select a type for each index.",
|
||||
};
|
||||
}
|
||||
|
||||
if (type && (!description || description.trim().length === 0)) {
|
||||
return {
|
||||
type: MongoNotificationType.Error,
|
||||
message: "Please enter a field name."
|
||||
message: "Please enter a field name.",
|
||||
};
|
||||
} else if (type === MongoIndexTypes.Wildcard && description?.indexOf("$**") === -1) {
|
||||
return {
|
||||
type: MongoNotificationType.Error,
|
||||
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder
|
||||
message: "Wildcard path is not present in the field name. Use a pattern like " + MongoWildcardPlaceHolder,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const collection = ({
|
||||
automatic: true,
|
||||
indexingMode: "default",
|
||||
includedPaths: [],
|
||||
excludedPaths: []
|
||||
excludedPaths: [],
|
||||
}),
|
||||
uniqueKeyPolicy: {} as DataModels.UniqueKeyPolicy,
|
||||
usageSizeInKB: ko.observable(100),
|
||||
@@ -24,7 +24,7 @@ export const collection = ({
|
||||
manualThroughput: 10000,
|
||||
minimumThroughput: 6000,
|
||||
id: "offer",
|
||||
offerReplacePending: false
|
||||
offerReplacePending: false,
|
||||
}),
|
||||
conflictResolutionPolicy: ko.observable<DataModels.ConflictResolutionPolicy>(
|
||||
{} as DataModels.ConflictResolutionPolicy
|
||||
@@ -37,10 +37,10 @@ export const collection = ({
|
||||
partitionKey: {
|
||||
paths: [],
|
||||
kind: "hash",
|
||||
version: 2
|
||||
version: 2,
|
||||
},
|
||||
partitionKeyProperty: "partitionKey",
|
||||
readSettings: () => {
|
||||
return;
|
||||
}
|
||||
},
|
||||
} as unknown) as ViewModels.Collection;
|
||||
|
||||
@@ -10,8 +10,8 @@ describe("SmartUiComponent", () => {
|
||||
message: "Start at $24/mo per database",
|
||||
link: {
|
||||
href: "https://aka.ms/azure-cosmos-db-pricing",
|
||||
text: "More Details"
|
||||
}
|
||||
text: "More Details",
|
||||
},
|
||||
},
|
||||
children: [
|
||||
{
|
||||
@@ -24,8 +24,8 @@ describe("SmartUiComponent", () => {
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
uiType: UiType.Spinner
|
||||
}
|
||||
uiType: UiType.Spinner,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "throughput2",
|
||||
@@ -37,8 +37,8 @@ describe("SmartUiComponent", () => {
|
||||
max: 500,
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
uiType: UiType.Slider
|
||||
}
|
||||
uiType: UiType.Slider,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "throughput3",
|
||||
@@ -51,16 +51,16 @@ describe("SmartUiComponent", () => {
|
||||
step: 10,
|
||||
defaultValue: 400,
|
||||
uiType: UiType.Spinner,
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'"
|
||||
}
|
||||
errorMessage: "label, truelabel and falselabel are required for boolean input 'throughput3'",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "containerId",
|
||||
input: {
|
||||
label: "Container id",
|
||||
dataFieldName: "containerId",
|
||||
type: "string"
|
||||
}
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "analyticalStore",
|
||||
@@ -70,8 +70,8 @@ describe("SmartUiComponent", () => {
|
||||
falseLabel: "Disabled",
|
||||
defaultValue: true,
|
||||
dataFieldName: "analyticalStore",
|
||||
type: "boolean"
|
||||
}
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "database",
|
||||
@@ -82,20 +82,20 @@ describe("SmartUiComponent", () => {
|
||||
choices: [
|
||||
{ label: "Database 1", key: "db1" },
|
||||
{ label: "Database 2", key: "db2" },
|
||||
{ label: "Database 3", key: "db3" }
|
||||
{ label: "Database 3", key: "db3" },
|
||||
],
|
||||
defaultKey: "db2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
defaultKey: "db2",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
it("should render", async () => {
|
||||
const wrapper = shallow(
|
||||
<SmartUiComponent descriptor={exampleData} currentValues={new Map()} onInputChange={undefined} />
|
||||
);
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export type InputTypeValue = "number" | "string" | "boolean" | "object";
|
||||
|
||||
export enum UiType {
|
||||
Spinner = "Spinner",
|
||||
Slider = "Slider"
|
||||
Slider = "Slider",
|
||||
}
|
||||
|
||||
export type ChoiceItem = { label: string; key: string };
|
||||
@@ -101,13 +101,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
private static readonly labelStyle = {
|
||||
color: "#393939",
|
||||
fontFamily: "wf_segoe-ui_normal, 'Segoe UI', 'Segoe WP', Tahoma, Arial, sans-serif",
|
||||
fontSize: 12
|
||||
fontSize: 12,
|
||||
};
|
||||
|
||||
constructor(props: SmartUiComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
errors: new Map()
|
||||
errors: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,10 +140,10 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
label: {
|
||||
root: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
}
|
||||
}
|
||||
}
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -200,7 +200,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
min: min,
|
||||
max: max,
|
||||
ariaLabel: label,
|
||||
step: step
|
||||
step: step,
|
||||
};
|
||||
|
||||
const value = this.props.currentValues.get(dataFieldName) as number;
|
||||
@@ -211,15 +211,15 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
{...props}
|
||||
id={`${input.dataFieldName}-spinner-input`}
|
||||
value={value?.toString()}
|
||||
onValidate={newValue => this.onValidate(input, newValue, props.min, props.max)}
|
||||
onIncrement={newValue => this.onIncrement(input, newValue, props.step, props.max)}
|
||||
onDecrement={newValue => this.onDecrement(input, newValue, props.step, props.min)}
|
||||
onValidate={(newValue) => this.onValidate(input, newValue, props.min, props.max)}
|
||||
onIncrement={(newValue) => this.onIncrement(input, newValue, props.step, props.max)}
|
||||
onDecrement={(newValue) => this.onDecrement(input, newValue, props.step, props.min)}
|
||||
labelPosition={Position.top}
|
||||
styles={{
|
||||
label: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
}
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{this.state.errors.has(dataFieldName) && (
|
||||
@@ -233,13 +233,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
<Slider
|
||||
{...props}
|
||||
value={value}
|
||||
onChange={newValue => this.props.onInputChange(input, newValue)}
|
||||
onChange={(newValue) => this.props.onInputChange(input, newValue)}
|
||||
styles={{
|
||||
titleLabel: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
fontWeight: 600,
|
||||
},
|
||||
valueLabel: SmartUiComponent.labelStyle
|
||||
valueLabel: SmartUiComponent.labelStyle,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -264,13 +264,13 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
{
|
||||
label: input.falseLabel,
|
||||
key: "false",
|
||||
onSelect: () => this.props.onInputChange(input, false)
|
||||
onSelect: () => this.props.onInputChange(input, false),
|
||||
},
|
||||
{
|
||||
label: input.trueLabel,
|
||||
key: "true",
|
||||
onSelect: () => this.props.onInputChange(input, true)
|
||||
}
|
||||
onSelect: () => this.props.onInputChange(input, true),
|
||||
},
|
||||
]}
|
||||
selectedKey={selectedKey}
|
||||
/>
|
||||
@@ -288,16 +288,16 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
selectedKey={value ? value : defaultKey}
|
||||
onChange={(_, item: IDropdownOption) => this.props.onInputChange(input, item.key.toString())}
|
||||
placeholder={placeholder}
|
||||
options={choices.map(c => ({
|
||||
options={choices.map((c) => ({
|
||||
key: c.key,
|
||||
text: c.label
|
||||
text: c.label,
|
||||
}))}
|
||||
styles={{
|
||||
label: {
|
||||
...SmartUiComponent.labelStyle,
|
||||
fontWeight: 600
|
||||
fontWeight: 600,
|
||||
},
|
||||
dropdown: SmartUiComponent.labelStyle
|
||||
dropdown: SmartUiComponent.labelStyle,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -334,7 +334,7 @@ export class SmartUiComponent extends React.Component<SmartUiComponentProps, Sma
|
||||
{node.info && this.renderInfo(node.info as Info)}
|
||||
{node.input && this.renderInput(node.input)}
|
||||
</Stack.Item>
|
||||
{node.children && node.children.map(child => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||
{node.children && node.children.map((child) => <div key={child.id}>{this.renderNode(child)}</div>)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export class TabComponent extends React.Component<TabComponentProps> {
|
||||
as="span"
|
||||
className={className}
|
||||
role="presentation"
|
||||
onActivated={e => this.setActiveTab(index)}
|
||||
onActivated={(e) => this.setActiveTab(index)}
|
||||
aria-label={`Select tab: ${tab.title}`}
|
||||
>
|
||||
{tab.title}
|
||||
|
||||
@@ -204,13 +204,13 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
this.label = options.label || ko.observable<string>();
|
||||
this.showAutoPilot = options.showAutoPilot !== undefined ? options.showAutoPilot : ko.observable<boolean>(true);
|
||||
this.isAutoPilotSelected = options.isAutoPilotSelected || ko.observable<boolean>(false);
|
||||
this.isAutoPilotSelected.subscribe(value => {
|
||||
this.isAutoPilotSelected.subscribe((value) => {
|
||||
TelemetryProcessor.trace(Action.ToggleAutoscaleSetting, ActionModifiers.Mark, {
|
||||
changedSelectedValueTo: value ? ActionModifiers.ToggleAutoscaleOn : ActionModifiers.ToggleAutoscaleOff,
|
||||
databaseAccountName: userContext.databaseAccount?.name,
|
||||
subscriptionId: userContext.subscriptionId,
|
||||
apiKind: userContext.defaultExperience,
|
||||
dataExplorerArea: "Scale Tab V1"
|
||||
dataExplorerArea: "Scale Tab V1",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,5 +310,5 @@ export class ThroughputInputViewModel extends WaitsForTemplateViewModel {
|
||||
|
||||
export const ThroughputInputComponentAutoPilotV3 = {
|
||||
viewModel: ThroughputInputViewModel,
|
||||
template: ThroughputInputComponentAutoscaleV3
|
||||
template: ThroughputInputComponentAutoscaleV3,
|
||||
};
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
</div>
|
||||
|
||||
<div class="freeTierInlineWarning" data-bind="visible: showFreeTierExceedThroughputWarning">
|
||||
<span class="freeTierWarningIcon"><img src="/warning.svg" alt="Warning"/></span>
|
||||
<span class="freeTierWarningIcon"><img src="/warning.svg" alt="Warning" /></span>
|
||||
<span class="freeTierWarningMessage" data-bind="text: freeTierExceedThroughputWarning"></span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,18 +4,18 @@ import { TreeComponent, TreeNode, TreeNodeComponent } from "./TreeComponent";
|
||||
|
||||
const buildChildren = (): TreeNode[] => {
|
||||
const grandChild11: TreeNode = {
|
||||
label: "ZgrandChild11"
|
||||
label: "ZgrandChild11",
|
||||
};
|
||||
const grandChild12: TreeNode = {
|
||||
label: "AgrandChild12"
|
||||
label: "AgrandChild12",
|
||||
};
|
||||
const child1: TreeNode = {
|
||||
label: "Bchild1",
|
||||
children: [grandChild11, grandChild12]
|
||||
children: [grandChild11, grandChild12],
|
||||
};
|
||||
|
||||
const child2: TreeNode = {
|
||||
label: "2child2"
|
||||
label: "2child2",
|
||||
};
|
||||
|
||||
return [child1, child2];
|
||||
@@ -23,28 +23,28 @@ const buildChildren = (): TreeNode[] => {
|
||||
|
||||
const buildChildren2 = (): TreeNode[] => {
|
||||
const grandChild11: TreeNode = {
|
||||
label: "ZgrandChild11"
|
||||
label: "ZgrandChild11",
|
||||
};
|
||||
const grandChild12: TreeNode = {
|
||||
label: "AgrandChild12"
|
||||
label: "AgrandChild12",
|
||||
};
|
||||
|
||||
const child1: TreeNode = {
|
||||
label: "aChild"
|
||||
label: "aChild",
|
||||
};
|
||||
|
||||
const child2: TreeNode = {
|
||||
label: "bchild",
|
||||
children: [grandChild11, grandChild12]
|
||||
children: [grandChild11, grandChild12],
|
||||
};
|
||||
|
||||
const child3: TreeNode = {
|
||||
label: "cchild"
|
||||
label: "cchild",
|
||||
};
|
||||
|
||||
const child4: TreeNode = {
|
||||
label: "dchild",
|
||||
children: [grandChild11, grandChild12]
|
||||
children: [grandChild11, grandChild12],
|
||||
};
|
||||
|
||||
return [child1, child2, child3, child4];
|
||||
@@ -54,12 +54,12 @@ describe("TreeComponent", () => {
|
||||
it("renders a simple tree", () => {
|
||||
const root = {
|
||||
label: "root",
|
||||
children: buildChildren()
|
||||
children: buildChildren(),
|
||||
};
|
||||
|
||||
const props = {
|
||||
rootNode: root,
|
||||
className: "tree"
|
||||
className: "tree",
|
||||
};
|
||||
|
||||
const wrapper = shallow(<TreeComponent {...props} />);
|
||||
@@ -78,8 +78,8 @@ describe("TreeNodeComponent", () => {
|
||||
label: "menuLabel",
|
||||
onClick: undefined,
|
||||
iconSrc: undefined,
|
||||
isDisabled: true
|
||||
}
|
||||
isDisabled: true,
|
||||
},
|
||||
],
|
||||
iconSrc: undefined,
|
||||
isExpanded: true,
|
||||
@@ -90,13 +90,13 @@ describe("TreeNodeComponent", () => {
|
||||
isSelected: undefined,
|
||||
onClick: undefined,
|
||||
onExpanded: undefined,
|
||||
onCollapsed: undefined
|
||||
onCollapsed: undefined,
|
||||
};
|
||||
|
||||
const props = {
|
||||
node,
|
||||
generation: 12,
|
||||
paddingLeft: 23
|
||||
paddingLeft: 23,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -106,12 +106,12 @@ describe("TreeNodeComponent", () => {
|
||||
const node: TreeNode = {
|
||||
label: "label",
|
||||
children: buildChildren(),
|
||||
isExpanded: true
|
||||
isExpanded: true,
|
||||
};
|
||||
const props = {
|
||||
node,
|
||||
generation: 2,
|
||||
paddingLeft: 9
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -121,12 +121,12 @@ describe("TreeNodeComponent", () => {
|
||||
const node: TreeNode = {
|
||||
label: "label",
|
||||
children: buildChildren(),
|
||||
isAlphaSorted: false
|
||||
isAlphaSorted: false,
|
||||
};
|
||||
const props = {
|
||||
node,
|
||||
generation: 2,
|
||||
paddingLeft: 9
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -148,13 +148,13 @@ describe("TreeNodeComponent", () => {
|
||||
isSelected: undefined,
|
||||
onClick: undefined,
|
||||
onExpanded: undefined,
|
||||
onCollapsed: undefined
|
||||
onCollapsed: undefined,
|
||||
};
|
||||
|
||||
const props = {
|
||||
node,
|
||||
generation: 12,
|
||||
paddingLeft: 23
|
||||
paddingLeft: 23,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
@@ -164,13 +164,13 @@ describe("TreeNodeComponent", () => {
|
||||
const node: TreeNode = {
|
||||
label: "label",
|
||||
children: [],
|
||||
isExpanded: true
|
||||
isExpanded: true,
|
||||
};
|
||||
|
||||
const props = {
|
||||
node,
|
||||
generation: 2,
|
||||
paddingLeft: 9
|
||||
paddingLeft: 9,
|
||||
};
|
||||
const wrapper = shallow(<TreeNodeComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
@@ -12,7 +12,7 @@ import { IconButton, IButtonStyles } from "office-ui-fabric-react/lib/Button";
|
||||
import {
|
||||
DirectionalHint,
|
||||
IContextualMenuItemProps,
|
||||
IContextualMenuProps
|
||||
IContextualMenuProps,
|
||||
} from "office-ui-fabric-react/lib/ContextualMenu";
|
||||
|
||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||
@@ -89,7 +89,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
this.isExpanded = props.node.isExpanded;
|
||||
this.state = {
|
||||
isExpanded: props.node.isExpanded,
|
||||
isMenuShowing: false
|
||||
isMenuShowing: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
if (this.props.node.isExpanded !== this.isExpanded) {
|
||||
this.isExpanded = this.props.node.isExpanded;
|
||||
this.setState({
|
||||
isExpanded: this.props.node.isExpanded
|
||||
isExpanded: this.props.node.isExpanded,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
let unsortedChildren;
|
||||
if (treeNode.isLeavesParentsSeparate) {
|
||||
// Separate parents and leave
|
||||
const parents: TreeNode[] = treeNode.children.filter(node => node.children);
|
||||
const leaves: TreeNode[] = treeNode.children.filter(node => !node.children);
|
||||
const parents: TreeNode[] = treeNode.children.filter((node) => node.children);
|
||||
const leaves: TreeNode[] = treeNode.children.filter((node) => !node.children);
|
||||
|
||||
if (treeNode.isAlphaSorted) {
|
||||
parents.sort(compareFct);
|
||||
@@ -235,7 +235,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
private renderContextMenuButton(node: TreeNode): JSX.Element {
|
||||
const menuItemLabel = "More";
|
||||
const buttonStyles: Partial<IButtonStyles> = {
|
||||
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` }
|
||||
rootFocused: { outline: `1px dashed ${Constants.StyleConstants.FocusColor}` },
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -246,7 +246,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
ariaLabel={menuItemLabel}
|
||||
menuIconProps={{
|
||||
iconName: menuItemLabel,
|
||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } }
|
||||
styles: { root: { fontSize: "18px", fontWeight: "bold" } },
|
||||
}}
|
||||
menuProps={{
|
||||
coverTarget: true,
|
||||
@@ -261,7 +261,7 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
<div
|
||||
data-test={`treeComponentMenuItemContainer`}
|
||||
className="treeComponentMenuItemContainer"
|
||||
onContextMenu={e => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||
onContextMenu={(e) => e.target.dispatchEvent(TreeNodeComponent.createClickEvent())}
|
||||
>
|
||||
{props.item.onRenderIcon()}
|
||||
<span
|
||||
@@ -281,11 +281,11 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||
onClick: () => {
|
||||
menuItem.onClick();
|
||||
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||
label: menuItem.label
|
||||
label: menuItem.label,
|
||||
});
|
||||
},
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||
}))
|
||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />,
|
||||
})),
|
||||
}}
|
||||
styles={buttonStyles}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user