Fixing smattering of Azure Browse bugs (#18197)

* fixing ability to type into filter boxes

* adding validation for combobox dropdowns

* loc updates

* clearing validation message

* lifting state to handle automatic first item selection

* Adding placeholders

* updating loc

* initial

* Revert "initial"

This reverts commit 0770b79573.

* adding icons for connection input modes

* adding docstrings

* swapping to b/w icon for Azure

* updating string

* loc updates

* fixing string

* loc updates
This commit is contained in:
Benjin Dubishar 2024-10-14 17:00:36 -07:00 коммит произвёл GitHub
Родитель d8eec82352
Коммит 1d539b46bc
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 281 добавлений и 51 удалений

Просмотреть файл

@ -189,16 +189,34 @@
"Browse Azure": "Browse Azure",
"Recent Connections": "Recent Connections",
"Subscription": "Subscription",
"subscription": "subscription",
"Resource Group": "Resource Group",
"resource group": "resource group",
"Location": "Location",
"location": "location",
"Server": "Server",
"server": "server",
"Database": "Database",
"database": "database",
"Filter Azure subscriptions": "Filter Azure subscriptions",
"Connection Error": "Connection Error",
"Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog.": "Encryption was enabled on this connection; review your SSL and certificate configuration for the target SQL Server, or enable 'Trust server certificate' in the connection dialog.",
"Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry?": "Note: A self-signed certificate offers only limited protection and is not a recommended practice for production environments. Do you want to enable 'Trust server certificate' on this connection and retry?",
"Read more": "Read more",
"Enable 'Trust Server Certificate'": "Enable 'Trust Server Certificate'",
"Select a {0} for filtering/{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'": {
"message": "Select a {0} for filtering",
"comment": [
"{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'"
]
},
"Select a valid {0} from the dropdown/{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'": {
"message": "Select a valid {0} from the dropdown",
"comment": [
"{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'"
]
},
"Default": "Default",
"Query {0}: Query cost (relative to the script): {1}%/{0} is the query number{1} is the query cost": {
"message": "Query {0}: Query cost (relative to the script): {1}%",
"comment": [

Просмотреть файл

@ -335,6 +335,9 @@
<trans-unit id="++CODE++e97a0773fb7b656c174155270bd9ded70bc82caa0fc5d65b3ba24871577a5b08">
<source xml:lang="en">Database name</source>
</trans-unit>
<trans-unit id="++CODE++21b111cbfe6e8fca2d181c43f53ad548b22e38aca955b9824706a504b0a07a2d">
<source xml:lang="en">Default</source>
</trans-unit>
<trans-unit id="++CODE++647cc4253428b25fe686fa8d5d5da5841fed53ad85395cdd43920d0226900c1d">
<source xml:lang="en">Default Value</source>
</trans-unit>
@ -1036,6 +1039,14 @@
<trans-unit id="++CODE++ebe0dbee443b4562c7925a01e3bc69a3d5d0a3b0e8b3e11041c69fa067b65ef7">
<source xml:lang="en">Select a tenant</source>
</trans-unit>
<trans-unit id="++CODE++8b43c4bf79e62cb8e9f61b4fddc6e921b87020df8bca991bbf77a39628ec9106">
<source xml:lang="en">Select a valid {0} from the dropdown</source>
<note>{0} is the type of the dropdown&apos;s contents, e.g &apos;resource group&apos; or &apos;server&apos;</note>
</trans-unit>
<trans-unit id="++CODE++a1205265db328a59a52187387e01e37a0e1d2b28210aa2e663f750eb8b7c8dcf">
<source xml:lang="en">Select a {0} for filtering</source>
<note>{0} is the type of the dropdown&apos;s contents, e.g &apos;resource group&apos; or &apos;server&apos;</note>
</trans-unit>
<trans-unit id="++CODE++1fc9a387654d410febd202b81bddbe62d38368260b69b9f73ee6630164536bdf">
<source xml:lang="en">Select all</source>
</trans-unit>
@ -1344,6 +1355,9 @@
<trans-unit id="++CODE++c1399614154a55dfb1fcb95e208ed1f0b1115b846d6150ded7c7b43fdf6cbc09">
<source xml:lang="en">authenticationType</source>
</trans-unit>
<trans-unit id="++CODE++3549b0028b75d981cdda2e573e9cb49dedc200185876df299f912b79f69dabd8">
<source xml:lang="en">database</source>
</trans-unit>
<trans-unit id="++CODE++c6072170d758e5358d717360829bd1f9b1603b355b5f7fe375d1aabdca7a20de">
<source xml:lang="en">encrypt</source>
</trans-unit>
@ -1353,9 +1367,21 @@
<trans-unit id="++CODE++075b0e112ce5f3585bf21875b678fb678865a30c79d482cd01098957e6497c28">
<source xml:lang="en">intelliSenseUpdated</source>
</trans-unit>
<trans-unit id="++CODE++e6eaea18e885e1078829b56df34896be5ab51439e8f0ba00cb1624b2c572c10e">
<source xml:lang="en">location</source>
</trans-unit>
<trans-unit id="++CODE++eb69f53f51eaef9887d5c3f3cbd0993b208e962fed51bdd572b0816eb75c1d10">
<source xml:lang="en">macOS Sierra or newer is required to use this feature.</source>
</trans-unit>
<trans-unit id="++CODE++35a1031e991d85c2d12e5768b98c031dcc6394f3ccfb766314b8a3d0ab2242db">
<source xml:lang="en">resource group</source>
</trans-unit>
<trans-unit id="++CODE++b3eacd33433b31b5252351032c9b3e7a2e7aa7738d5decdf0dd6c62680853c06">
<source xml:lang="en">server</source>
</trans-unit>
<trans-unit id="++CODE++a8fa7fd60893411a49907b5545ccf9c47ed8073cc38e2ac3b79c4f6156cd7755">
<source xml:lang="en">subscription</source>
</trans-unit>
<trans-unit id="++CODE++9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08">
<source xml:lang="en">test</source>
</trans-unit>

Просмотреть файл

@ -150,11 +150,16 @@ export class LocConstants {
connectionString: l10n.t("Connection String"),
browseAzure: l10n.t("Browse Azure"),
recentConnections: l10n.t("Recent Connections"),
subscription: l10n.t("Subscription"),
resourceGroup: l10n.t("Resource Group"),
location: l10n.t("Location"),
server: l10n.t("Server"),
database: l10n.t("Database"),
subscriptionLabel: l10n.t("Subscription"),
subscription: l10n.t("subscription"),
resourceGroupLabel: l10n.t("Resource Group"),
resourceGroup: l10n.t("resource group"),
locationLabel: l10n.t("Location"),
location: l10n.t("location"),
serverLabel: l10n.t("Server"),
server: l10n.t("server"),
databaseLabel: l10n.t("Database"),
database: l10n.t("database"),
filterSubscriptions: l10n.t("Filter Azure subscriptions"),
connectionErrorTitle: l10n.t("Connection Error"),
trustServerCertMessage: l10n.t(
@ -168,6 +173,23 @@ export class LocConstants {
"Enable 'Trust Server Certificate'",
),
close: l10n.t("Close"),
azureFilterPlaceholder: (dropdownContentType: string) =>
l10n.t({
message: "Select a {0} for filtering",
args: [dropdownContentType],
comment: [
"{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'",
],
}),
invalidAzureBrowse: (dropdownContentType: string) =>
l10n.t({
message: "Select a valid {0} from the dropdown",
args: [dropdownContentType],
comment: [
"{0} is the type of the dropdown's contents, e.g 'resource group' or 'server'",
],
}),
default: l10n.t("Default"),
};
}

Просмотреть файл

@ -41,3 +41,11 @@ export function getVscodeThemeType(theme: Theme): string {
return "light";
}
}
export function themeType(theme: Theme): string {
const themeType = getVscodeThemeType(theme);
if (themeType !== "light") {
return "dark";
}
return themeType;
}

Просмотреть файл

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.02528 0.4902V0.490067H7.05254H13.0018C13.5852 0.490067 14.1037 0.862249 14.2902 1.41505L19.4534 16.7132C19.7509 17.5946 19.0952 18.5078 18.1649 18.5078H12.3371V18.5026C12.2972 18.5061 12.2567 18.5078 12.2157 18.5078H12.1933C11.9012 18.5078 11.6169 18.4138 11.3824 18.2396L7.58436 15.4182L6.85375 17.583C6.66717 18.1358 6.14875 18.508 5.56532 18.508H1.83498C0.904676 18.508 0.249061 17.5948 0.546554 16.7133L5.70967 1.41519C5.89624 0.862385 6.41466 0.4902 6.9981 0.4902H7.02528ZM6.50171 13.3435L11.9906 17.4209C12.0492 17.4645 12.1203 17.488 12.1933 17.488H12.2157C12.4482 17.488 12.6121 17.2597 12.5378 17.0393L9.75692 8.79976L8.64445 11.6791L8.51843 12.0053H8.16878H4.7211L6.50171 13.3435ZM18.1649 17.488H13.5332C13.5968 17.2438 13.5931 16.977 13.5041 16.7132L8.373 1.50994H13.0018C13.1477 1.50994 13.2773 1.60299 13.3239 1.74119L18.487 17.0393C18.5614 17.2597 18.3975 17.488 18.1649 17.488Z" fill="white"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.0 KiB

Просмотреть файл

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.02528 0.490185V0.490051H7.05254H13.0018C13.5852 0.490051 14.1037 0.862234 14.2902 1.41504L19.4534 16.7132C19.7509 17.5946 19.0952 18.5078 18.1649 18.5078H12.3371V18.5026C12.2972 18.5061 12.2567 18.5078 12.2157 18.5078H12.1933C11.9012 18.5078 11.6169 18.4138 11.3824 18.2396L7.58436 15.4182L6.85375 17.583C6.66717 18.1358 6.14875 18.508 5.56532 18.508H1.83498C0.904676 18.508 0.249061 17.5947 0.546554 16.7133L5.70967 1.41517C5.89624 0.86237 6.41466 0.490185 6.9981 0.490185H7.02528ZM6.50171 13.3434L11.9906 17.4209C12.0492 17.4644 12.1203 17.488 12.1933 17.488H12.2157C12.4482 17.488 12.6121 17.2597 12.5378 17.0393L9.75692 8.79974L8.64445 11.6791L8.51843 12.0053H8.16878H4.7211L6.50171 13.3434ZM18.1649 17.488H13.5332C13.5968 17.2437 13.5931 16.977 13.5041 16.7132L8.373 1.50993H13.0018C13.1477 1.50993 13.2773 1.60297 13.3239 1.74117L18.487 17.0393C18.5614 17.2597 18.3975 17.488 18.1649 17.488Z" fill="black"/>
</svg>

После

Ширина:  |  Высота:  |  Размер: 1.0 KiB

Просмотреть файл

@ -13,6 +13,9 @@ import {
Combobox,
Spinner,
makeStyles,
ComboboxProps,
OptionOnSelectData,
SelectionEvents,
} from "@fluentui/react-components";
import { Filter16Filled } from "@fluentui/react-icons";
import { FormField, useFormStyles } from "../../common/forms/form.component";
@ -39,26 +42,33 @@ export const AzureBrowsePage = () => {
const [selectedSubscription, setSelectedSubscription] = useState<
string | undefined
>(undefined);
const [subscriptionValue, setSubscriptionValue] = useState<string>("");
const [resourceGroups, setResourceGroups] = useState<string[]>([]);
const [selectedResourceGroup, setSelectedResourceGroup] = useState<
string | undefined
>(undefined);
const [resourceGroupValue, setResourceGroupValue] = useState<string>("");
const [locations, setLocations] = useState<string[]>([]);
const [selectedLocation, setSelectedLocation] = useState<
string | undefined
>(undefined);
const [locationValue, setLocationValue] = useState<string>("");
const [servers, setServers] = useState<string[]>([]);
const [selectedServer, setSelectedServer] = useState<string | undefined>(
undefined,
);
const [serverValue, setServerValue] = useState<string>("");
const [databases, setDatabases] = useState<string[]>([]);
const [selectedDatabase, setSelectedDatabase] = useState<
string | undefined
>(undefined);
const [databaseValue, setDatabaseValue] = useState<string>("");
// #region Effects
useEffect(() => {
const subs = removeDuplicates(
@ -148,10 +158,19 @@ export const AzureBrowsePage = () => {
);
setServers(srvs.sort());
// if current selection is no longer in the list of options,
// set selection to undefined (if multiple options) or the only option (if only one)
if (selectedServer && !srvs.includes(selectedServer)) {
setSelectedServer(srvs.length === 1 ? srvs[0] : undefined);
// intentionally cleared
if (selectedServer === "") {
setServerValue("");
} else {
// if there is no current selection or if the selection is no longer in the list of options (due to changed filters),
// set selection to the first option (if any)
if (
selectedServer === undefined ||
!srvs.includes(selectedServer)
) {
setSelectedServer(srvs.length > 0 ? srvs[0] : undefined);
setServerValue(srvs.length > 0 ? srvs[0] : "");
}
}
}, [locations, selectedLocation, context.state.azureServers]);
@ -174,6 +193,8 @@ export const AzureBrowsePage = () => {
setSelectedDatabase(dbs.length === 1 ? dbs[0] : undefined);
}, [selectedServer]);
// #endregion
function setConnectionProperty(
propertyName: keyof IConnectionDialogProfile,
value: string,
@ -184,7 +205,7 @@ export const AzureBrowsePage = () => {
return (
<div>
<AzureBrowseDropdown
label={Loc.connectionDialog.subscription}
label={Loc.connectionDialog.subscriptionLabel}
clearable
decoration={
<>
@ -204,7 +225,10 @@ export const AzureBrowsePage = () => {
}
content={{
valueList: subscriptions,
setValue: (sub) => {
value: subscriptionValue,
setValue: setSubscriptionValue,
selection: selectedSubscription,
setSelection: (sub) => {
setSelectedSubscription(sub);
if (sub === undefined) {
@ -232,29 +256,53 @@ export const AzureBrowsePage = () => {
context.loadAzureServers(subId);
},
currentValue: selectedSubscription,
placeholder: Loc.connectionDialog.azureFilterPlaceholder(
Loc.connectionDialog.subscription,
),
invalidOptionErrorMessage:
Loc.connectionDialog.invalidAzureBrowse(
Loc.connectionDialog.subscription,
),
}}
/>
<AzureBrowseDropdown
label={Loc.connectionDialog.resourceGroup}
label={Loc.connectionDialog.resourceGroupLabel}
clearable
content={{
valueList: resourceGroups,
setValue: setSelectedResourceGroup,
currentValue: selectedResourceGroup,
value: resourceGroupValue,
setValue: setResourceGroupValue,
selection: selectedResourceGroup,
setSelection: setSelectedResourceGroup,
placeholder: Loc.connectionDialog.azureFilterPlaceholder(
Loc.connectionDialog.resourceGroup,
),
invalidOptionErrorMessage:
Loc.connectionDialog.invalidAzureBrowse(
Loc.connectionDialog.resourceGroup,
),
}}
/>
<AzureBrowseDropdown
label={Loc.connectionDialog.location}
label={Loc.connectionDialog.locationLabel}
clearable
content={{
valueList: locations,
setValue: setSelectedLocation,
currentValue: selectedLocation,
value: locationValue,
setValue: setLocationValue,
selection: selectedLocation,
setSelection: setSelectedLocation,
placeholder: Loc.connectionDialog.azureFilterPlaceholder(
Loc.connectionDialog.location,
),
invalidOptionErrorMessage:
Loc.connectionDialog.invalidAzureBrowse(
Loc.connectionDialog.location,
),
}}
/>
<AzureBrowseDropdown
label={Loc.connectionDialog.server}
label={Loc.connectionDialog.serverLabel}
required
decoration={
context.state.loadingAzureServersStatus ===
@ -264,14 +312,20 @@ export const AzureBrowsePage = () => {
}
content={{
valueList: servers,
setValue: (srv) => {
value: serverValue,
setValue: setServerValue,
selection: selectedServer,
setSelection: (srv) => {
setSelectedServer(srv);
setConnectionProperty(
"server",
srv ? srv + ".database.windows.net" : "",
);
},
currentValue: selectedServer,
invalidOptionErrorMessage:
Loc.connectionDialog.invalidAzureBrowse(
Loc.connectionDialog.server,
),
}}
/>
@ -288,15 +342,22 @@ export const AzureBrowsePage = () => {
props={{ orientation: "horizontal" }}
/>
<AzureBrowseDropdown
label={Loc.connectionDialog.database}
label={Loc.connectionDialog.databaseLabel}
clearable
content={{
valueList: databases,
setValue: (db) => {
value: databaseValue,
setValue: setDatabaseValue,
selection: selectedDatabase,
setSelection: (db) => {
setSelectedDatabase(db);
setConnectionProperty("database", db ?? "");
},
currentValue: selectedDatabase,
placeholder: `<${Loc.connectionDialog.default}>`,
invalidOptionErrorMessage:
Loc.connectionDialog.invalidAzureBrowse(
Loc.connectionDialog.database,
),
}}
/>
{context.state.connectionComponents.mainOptions
@ -368,24 +429,66 @@ const AzureBrowseDropdown = ({
clearable,
content,
decoration,
props,
}: {
label: string;
required?: boolean;
clearable?: boolean;
content: {
/** list of valid values for the combo box */
valueList: string[];
setValue: (value: string | undefined) => void;
currentValue?: 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>("");
const onInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
content.setValue(ev.target.value);
// 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
@ -401,21 +504,19 @@ const AzureBrowseDropdown = ({
}
orientation="horizontal"
required={required}
validationMessage={validationMessage}
onBlur={onBlur}
>
<Combobox
value={content.currentValue ?? ""}
{...props}
value={content.value}
selectedOptions={
content.currentValue ? [content.currentValue] : []
content.selection ? [content.selection] : []
}
clearable={clearable}
onInput={onInput}
onOptionSelect={(_event, data) => {
if (data.optionValue === content.currentValue) {
return;
}
content.setValue(data.optionValue);
}}
onOptionSelect={onOptionSelect}
placeholder={content.placeholder}
clearable={clearable}
>
{content.valueList.map((val, idx) => {
return (

Просмотреть файл

@ -10,7 +10,10 @@ import {
MessageBar,
Radio,
RadioGroup,
Image,
Theme,
} from "@fluentui/react-components";
import { SlideText20Regular, Form20Regular } from "@fluentui/react-icons";
import {
ConnectionDialogContextProps,
IConnectionDialogProfile,
@ -26,6 +29,7 @@ import { FormItemSpec } from "../../common/forms/form";
import { locConstants } from "../../common/locConstants";
import { AzureBrowsePage } from "./azureBrowsePage";
import { TrustServerCertificateDialog } from "./components/trustServerCertificateDialog.component";
import { themeType } from "../../common/utils";
function renderContent(
connectionDialogContext: ConnectionDialogContextProps,
@ -44,6 +48,15 @@ export const ConnectionInfoFormContainer = () => {
const context = useContext(ConnectionDialogContext)!;
const formStyles = useFormStyles();
function azureIcon(colorTheme: Theme) {
const theme = themeType(colorTheme);
const saveIcon =
theme === "dark"
? require("../../media/azure-inverse.svg")
: require("../../media/azure.svg");
return saveIcon;
}
return (
<div className={formStyles.formRoot}>
<ConnectionHeader />
@ -78,19 +91,63 @@ export const ConnectionInfoFormContainer = () => {
>
<Radio
value={ConnectionInputMode.Parameters}
label={locConstants.connectionDialog.parameters}
label={
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<Form20Regular
style={{ marginRight: "8px" }}
/>
{
locConstants.connectionDialog
.parameters
}
</div>
}
/>
<Radio
value={ConnectionInputMode.ConnectionString}
label={
locConstants.connectionDialog
.connectionString
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<SlideText20Regular
style={{ marginRight: "8px" }}
/>
{
locConstants.connectionDialog
.connectionString
}
</div>
}
/>
<Radio
value={ConnectionInputMode.AzureBrowse}
label={
locConstants.connectionDialog.browseAzure
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<Image
src={azureIcon(context.theme)}
alt="Azure"
height={20}
width={20}
style={{ marginRight: "8px" }}
/>
{
locConstants.connectionDialog
.browseAzure
}
</div>
}
/>
</RadioGroup>

Просмотреть файл

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Theme } from "@fluentui/react-components";
import { getVscodeThemeType } from "../../common/utils";
import { themeType } from "../../common/utils";
const iterator_catch_all = require("./icons/iterator_catch_all.png");
const cursor_catch_all = require("./icons/cursor_catch_all.png");
@ -398,14 +398,6 @@ export function getCollapseExpandPaths() {
};
}
function themeType(theme: Theme): string {
const themeType = getVscodeThemeType(theme);
if (themeType !== "light") {
return "dark";
}
return themeType;
}
export const save = (colorTheme: Theme) => {
const theme = themeType(colorTheme);
const saveIcon =