Skip to content

Improving the azure browse filter dropdown logic #18281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/connectionconfig/connectionDialogWebviewController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1138,14 +1138,16 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
string[] | undefined
>(azureSubscriptionFilterConfigKey) !== undefined;

const startTime = Date.now();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should create a helper function to track time. It would return another function that, when called, automatically measures the elapsed time and sends the action event with the mentioned properties.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this thought. I'll make an issue to add some util stuff like that.


this._azureSubscriptions = new Map(
(await auth.getSubscriptions(shouldUseFilter)).map((s) => [
s.subscriptionId,
s,
]),
);
const tenantSubMap = this.groupBy<string, AzureSubscription>(
await auth.getSubscriptions(shouldUseFilter),
Array.from(this._azureSubscriptions.values()),
"tenantId",
); // TODO: replace with Object.groupBy once ES2024 is supported

Expand All @@ -1164,13 +1166,31 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
state.azureSubscriptions = subs;
state.loadingAzureSubscriptionsStatus = ApiStatus.Loaded;

sendActionEvent(
TelemetryViews.ConnectionDialog,
TelemetryActions.LoadAzureSubscriptions,
undefined, // additionalProperties
{
subscriptionCount: subs.length,
msToLoadSubscriptions: Date.now() - startTime,
},
);

this.updateState();

return tenantSubMap;
} catch (error) {
state.formError = l10n.t("Error loading Azure subscriptions.");
state.loadingAzureSubscriptionsStatus = ApiStatus.Error;
console.error(state.formError + "\n" + getErrorMessage(error));

sendErrorEvent(
TelemetryViews.ConnectionDialog,
TelemetryActions.LoadAzureSubscriptions,
error,
false, // includeErrorMessage
);

return undefined;
}
}
Expand All @@ -1179,7 +1199,6 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
state: ConnectionDialogWebviewState,
): Promise<void> {
try {
const startTime = Date.now();
const tenantSubMap = await this.loadAzureSubscriptions(state);

if (!tenantSubMap) {
Expand All @@ -1192,8 +1211,11 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
);
} else {
state.loadingAzureServersStatus = ApiStatus.Loading;
state.azureServers = [];
this.updateState();

const startTime = Date.now();

const promiseArray: Promise<void>[] = [];
for (const t of tenantSubMap.keys()) {
for (const s of tenantSubMap.get(t)) {
Expand Down Expand Up @@ -1229,12 +1251,7 @@ export class ConnectionDialogWebviewController extends ReactWebviewPanelControll
TelemetryViews.ConnectionDialog,
TelemetryActions.LoadAzureServers,
error,
true, // includeErrorMessage
undefined, // errorCode
undefined, // errorType
{
connectionInputType: this.state.selectedInputMode,
},
false, // includeErrorMessage
);

return;
Expand Down
56 changes: 56 additions & 0 deletions src/reactviews/common/comboboxHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/** Behavior for how the default selection is determined */
export enum DefaultSelectionMode {
/** If there are any options, the first is always selected. Otherwise, selects nothing. */
SelectFirstIfAny,
/** Always selects nothing, regardless of if there are available options */
AlwaysSelectNone,
/** Selects the only option if there's only one. Otherwise (many or no options) selects nothing. */
SelectOnlyOrNone,
}

export function updateComboboxSelection(
/** current selected (valid) option */
selected: string | undefined,
/** callback to set the selected (valid) option */
setSelected: (s: string | undefined) => void,
/** callback to set the displayed value (not guaranteed to be valid if the user has manually typed something) */
setValue: (v: string) => void,
/** list of valid options */
optionList: string[],
/** behavior for choosing the default selected value */
defaultSelectionMode: DefaultSelectionMode = DefaultSelectionMode.AlwaysSelectNone,
) {
// if there is no current selection or if the current selection is no longer in the list of options (due to filter changes),
// then select the only option if there is only one option, then make a default selection according to specified `defaultSelectionMode`

if (
selected === undefined ||
(selected && !optionList.includes(selected))
) {
let optionToSelect: string | undefined = undefined;

if (optionList.length > 0) {
switch (defaultSelectionMode) {
case DefaultSelectionMode.SelectFirstIfAny:
optionToSelect =
optionList.length > 0 ? optionList[0] : undefined;
break;
case DefaultSelectionMode.SelectOnlyOrNone:
optionToSelect =
optionList.length === 1 ? optionList[0] : undefined;
break;
case DefaultSelectionMode.AlwaysSelectNone:
default:
optionToSelect = undefined;
}
}

setSelected(optionToSelect); // selected value's unselected state should be undefined
setValue(optionToSelect ?? ""); // displayed value's unselected state should be an empty string
}
}
5 changes: 5 additions & 0 deletions src/reactviews/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ export function themeType(theme: Theme): string {
}
return themeType;
}

/** Removes duplicate values from an array */
export function removeDuplicates<T>(array: T[]): T[] {
return Array.from(new Set(array));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import {
Combobox,
ComboboxProps,
Field,
makeStyles,
OptionOnSelectData,
SelectionEvents,
Option,
} from "@fluentui/react-components";
import { useFormStyles } from "../../common/forms/form.component";
import { useEffect, useState } from "react";

const useFieldDecorationStyles = makeStyles({
decoration: {
display: "flex",
alignItems: "center",
columnGap: "4px",
},
});

export const AzureFilterCombobox = ({
label,
required,
clearable,
content,
decoration,
props,
}: {
label: string;
required?: boolean;
clearable?: boolean;
content: {
/** list of valid values for the combo box */
valueList: string[];
/** currently-selected value from `valueList` */
selection?: string;
/** callback when the user has selected a value from `valueList` */
setSelection: (value: string | undefined) => void;
/** currently-entered text in the combox, may not be a valid selection value if the user is typing */
value: string;
/** callback when the user types in the combobox */
setValue: (value: string) => void;
/** placeholder text for the combobox */
placeholder?: string;
/** message displayed if focus leaves this combobox and `value` is not a valid value from `valueList` */
invalidOptionErrorMessage: string;
};
decoration?: JSX.Element;
props?: Partial<ComboboxProps>;
}) => {
const formStyles = useFormStyles();
const decorationStyles = useFieldDecorationStyles();
const [validationMessage, setValidationMessage] = useState<string>("");

// clear validation message as soon as value is valid
useEffect(() => {
if (content.valueList.includes(content.value)) {
setValidationMessage("");
}
}, [content.value]);

// only display validation error if focus leaves the field and the value is not valid
const onBlur = () => {
if (content.value) {
setValidationMessage(
content.valueList.includes(content.value)
? ""
: content.invalidOptionErrorMessage,
);
}
};

const onOptionSelect: (
_: SelectionEvents,
data: OptionOnSelectData,
) => void = (_, data: OptionOnSelectData) => {
content.setSelection(
data.selectedOptions.length > 0 ? data.selectedOptions[0] : "",
);
content.setValue(data.optionText ?? "");
};

function onInput(ev: React.ChangeEvent<HTMLInputElement>) {
content.setValue(ev.target.value);
}

return (
<div className={formStyles.formComponentDiv}>
<Field
label={
decoration ? (
<div className={decorationStyles.decoration}>
{label}
{decoration}
</div>
) : (
label
)
}
orientation="horizontal"
required={required}
validationMessage={validationMessage}
onBlur={onBlur}
>
<Combobox
{...props}
value={content.value}
selectedOptions={
content.selection ? [content.selection] : []
}
onInput={onInput}
onOptionSelect={onOptionSelect}
placeholder={content.placeholder}
clearable={clearable}
>
{content.valueList.map((val, idx) => {
return (
<Option key={idx} value={val}>
{val}
</Option>
);
})}
</Combobox>
</Field>
</div>
);
};
Loading
Loading