Skip to content

Commit c6bae01

Browse files
authored
Improving VS Code account loading states for Connection Dialog and Add Firewall Dialog (#21861)
* Fixing spinner alignment * removing hardcode * adding caching to Entra account load logic * cleanup * fixed up add firewall dialog * Loc * PR cleanup * Adding sign-in button in banner for old Entra account system * Updating test
1 parent 0248266 commit c6bae01

15 files changed

Lines changed: 436 additions & 153 deletions

File tree

extensions/mssql/l10n/bundle.l10n.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"Warning": "Warning",
6363
"Sign In": "Sign In",
6464
"Loading": "Loading",
65+
"Loading...": "Loading...",
6566
"General": "General",
6667
"Previous": "Previous",
6768
"OK": "OK",
@@ -209,6 +210,7 @@
209210
"comment": ["{0} is the IP address of the client"]
210211
},
211212
"Add my subnet IP range": "Add my subnet IP range",
213+
"IP address range": "IP address range",
212214
"From/Label for the start IP address in the firewall rule IP range": {
213215
"message": "From",
214216
"comment": ["Label for the start IP address in the firewall rule IP range"]
@@ -885,7 +887,6 @@
885887
"Custom GraphQL Type": "Custom GraphQL Type",
886888
"Optional - Override default GraphQL type name": "Optional - Override default GraphQL type name",
887889
"Source Table": "Source Table",
888-
"Loading...": "Loading...",
889890
"Initializing DAB configuration...": "Initializing DAB configuration...",
890891
"No entities found": "No entities found",
891892
"Toggle all entities in {0}/{0} is the schema name": {
@@ -2286,6 +2287,10 @@
22862287
"message": "No SQL resource is configured for the current cloud '{0}'. Please update your Azure account settings.",
22872288
"comment": ["{0} is the display name of the current cloud"]
22882289
},
2290+
"Azure account '{0}' was not found. Sign in with the correct account or select a different one./{0} is the display name or ID of the Azure account that was not found": {
2291+
"message": "Azure account '{0}' was not found. Sign in with the correct account or select a different one.",
2292+
"comment": ["{0} is the display name or ID of the Azure account that was not found"]
2293+
},
22892294
"Error signing into Azure: {0}/{0} is the error message": {
22902295
"message": "Error signing into Azure: {0}",
22912296
"comment": ["{0} is the error message"]

extensions/mssql/src/connectionconfig/connectionDialogWebviewController.ts

Lines changed: 254 additions & 80 deletions
Large diffs are not rendered by default.

extensions/mssql/src/constants/locConstants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,6 +1074,15 @@ export class Azure {
10741074
});
10751075
}
10761076

1077+
public static accountNotFound(accountDisplayName: string): string {
1078+
return l10n.t({
1079+
message:
1080+
"Azure account '{0}' was not found. Sign in with the correct account or select a different one.",
1081+
args: [accountDisplayName],
1082+
comment: ["{0} is the display name or ID of the Azure account that was not found"],
1083+
});
1084+
}
1085+
10771086
public static errorSigningIntoAzure(errorMessage: string): string {
10781087
return l10n.t({
10791088
message: "Error signing into Azure: {0}",

extensions/mssql/src/controllers/addFirewallRuleWebviewController.ts

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class AddFirewallRuleWebviewController extends WebviewPanelController<
4747
message: initializationProps.errorMessage,
4848
clientIp: "",
4949
isSignedIn: false,
50+
loadingAccounts: true,
5051
accounts: [],
5152
tenants: {},
5253
addFirewallRuleStatus: ApiStatus.NotStarted,
@@ -90,14 +91,8 @@ export class AddFirewallRuleWebviewController extends WebviewPanelController<
9091
* Initialize the controller
9192
*/
9293
private async initializeDialog(errorMessage: string): Promise<void> {
93-
// Check if user is signed into Azure, and populate the dialog if they are
94-
this.state.isSignedIn = await VsCodeAzureHelper.isSignedIn();
94+
const accountLoadingPromise = this.loadAzureAccounts(); // Load accounts and extract client IP in parallel
9595

96-
if (this.state.isSignedIn) {
97-
await populateAzureAccountInfo(this.state, false /* forceSignInPrompt */);
98-
}
99-
100-
// Extract the client IP address from the error message
10196
const handleFirewallErrorResult = await this.firewallService.handleFirewallRule(
10297
errorFirewallRule,
10398
errorMessage,
@@ -118,6 +113,21 @@ export class AddFirewallRuleWebviewController extends WebviewPanelController<
118113
}
119114

120115
this.state.clientIp = handleFirewallErrorResult.ipAddress;
116+
117+
await accountLoadingPromise;
118+
}
119+
120+
private async loadAzureAccounts(): Promise<void> {
121+
try {
122+
this.state.isSignedIn = await VsCodeAzureHelper.isSignedIn();
123+
124+
if (this.state.isSignedIn) {
125+
await populateAzureAccountInfo(this.state, false /* forceSignInPrompt */);
126+
}
127+
} finally {
128+
this.state.loadingAccounts = false;
129+
this.updateState();
130+
}
121131
}
122132

123133
/**
@@ -165,7 +175,14 @@ export class AddFirewallRuleWebviewController extends WebviewPanelController<
165175
});
166176

167177
this.registerReducer("signIntoAzure", async (state) => {
168-
await populateAzureAccountInfo(state, true /* forceSignInPrompt */);
178+
state.loadingAccounts = true;
179+
this.updateState(state);
180+
181+
try {
182+
await populateAzureAccountInfo(state, true /* forceSignInPrompt */);
183+
} finally {
184+
state.loadingAccounts = false;
185+
}
169186

170187
return state;
171188
});
@@ -181,11 +198,7 @@ export async function populateAzureAccountInfo(
181198
try {
182199
auth = await VsCodeAzureHelper.signIn(forceSignInPrompt);
183200
} catch (error) {
184-
this.logger.error(`Error signing into Azure: ${getErrorMessage(error)}`);
185-
this.vscodeWrapper.showErrorMessage(
186-
Loc.Azure.errorSigningIntoAzure(getErrorMessage(error)),
187-
);
188-
201+
console.error(`Error signing into Azure: ${getErrorMessage(error)}`);
189202
return;
190203
}
191204

extensions/mssql/src/objectExplorer/objectExplorerService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,7 @@ export class ObjectExplorerService {
762762
);
763763
if (choice === LocalizedConstants.ObjectExplorer.FailedOEConnectionErrorSignIn) {
764764
try {
765-
await VsCodeAzureHelper.signIn(true); // User chose to sign in to the missing account; try again.
765+
await VsCodeAzureHelper.signIn(true /* forceSignInPrompt */); // User chose to sign in to the missing account; try again.
766766
return await prepareConnectionProfile();
767767
} catch (retryError) {
768768
this._logger.error(

extensions/mssql/src/protocol.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@ export class Deferred<T> {
88
promise: Promise<T>;
99
resolve: (value?: T | PromiseLike<T>) => void;
1010
reject: (reason?: any) => void;
11+
isCompleted = false;
1112
constructor() {
1213
this.promise = new Promise<T>((resolve, reject) => {
13-
this.resolve = resolve;
14-
this.reject = reject;
14+
this.resolve = (value?: T | PromiseLike<T>) => {
15+
this.isCompleted = true;
16+
resolve(value);
17+
};
18+
this.reject = (reason?: any) => {
19+
this.isCompleted = true;
20+
reject(reason);
21+
};
1522
});
1623
}
1724

extensions/mssql/src/sharedInterfaces/addFirewallRule.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface AddFirewallRuleState {
1515
message: string;
1616
clientIp: string;
1717
isSignedIn: boolean;
18+
/** Whether accounts and tenants are currently being loaded */
19+
loadingAccounts: boolean;
1820
accounts: IMssqlAzureAccount[];
1921
/** Maps from account ID to list of tenants */
2022
tenants: Record<string, IMssqlAzureTenant[]>;

extensions/mssql/src/sharedInterfaces/form.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ export interface FormItemSpec<
8888
* Whether the form item is an advanced option
8989
*/
9090
isAdvancedOption?: boolean;
91+
92+
/**
93+
* Whether the form item is currently loading its data (e.g. dropdown options)
94+
*/
95+
loading?: boolean;
9196
}
9297

9398
export interface FormItemValidationState {

extensions/mssql/src/webviews/common/forms/form.component.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import {
1212
FieldProps,
1313
InfoLabel,
1414
Input,
15+
Label,
1516
LabelProps,
1617
Option,
18+
Spinner,
1719
Text,
1820
Textarea,
1921
makeStyles,
@@ -73,6 +75,11 @@ export const useFormStyles = makeStyles({
7375
display: "flex",
7476
marginLeft: "auto",
7577
},
78+
labelDecoration: {
79+
display: "inline-flex",
80+
alignItems: "center",
81+
columnGap: "0px",
82+
},
7683
});
7784

7885
export const FormInput = <
@@ -218,29 +225,29 @@ export const FormField = <
218225
}
219226
required={component.required}
220227
// @ts-ignore there's a bug in the typings somewhere, so ignoring this line to avoid angering type-checker
221-
label={
228+
label={{
222229
// The html here shouldn't need to be sanitized, and should be safe
223230
// because it's only ever set by forms internal to the extension
224-
component.tooltip ? (
225-
{
226-
children: (_: unknown, slotProps: LabelProps) => (
227-
<InfoLabel {...slotProps} info={component.tooltip}>
228-
<span
229-
dangerouslySetInnerHTML={{
230-
__html: component.label,
231-
}}
232-
/>
233-
</InfoLabel>
234-
),
235-
}
236-
) : (
237-
<span
238-
dangerouslySetInnerHTML={{
239-
__html: component.label,
240-
}}
241-
/>
242-
)
243-
}
231+
children: (_: unknown, slotProps: LabelProps) => {
232+
const labelContent = (
233+
<span
234+
dangerouslySetInnerHTML={{
235+
__html: component.label,
236+
}}
237+
/>
238+
);
239+
const LabelComponent = component.tooltip ? InfoLabel : Label;
240+
const tooltipProps = component.tooltip ? { info: component.tooltip } : {};
241+
return (
242+
<span className={formStyles.labelDecoration}>
243+
<LabelComponent {...slotProps} {...tooltipProps}>
244+
{labelContent}
245+
</LabelComponent>
246+
{component.loading && <Spinner size="extra-tiny" />}
247+
</span>
248+
);
249+
},
250+
}}
244251
{...props}
245252
style={{ color: tokens.colorNeutralForeground1 }}>
246253
{generateFormComponent<TForm, TState, TFormItemSpec, TContext>(

extensions/mssql/src/webviews/common/locConstants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class LocConstants {
6767
warning: l10n.t("Warning"),
6868
signIn: l10n.t("Sign In"),
6969
loading: l10n.t("Loading"),
70+
loadingWithEllipsis: l10n.t("Loading..."),
7071
general: l10n.t("General"),
7172
previous: l10n.t("Previous"),
7273
ok: l10n.t("OK"),
@@ -300,6 +301,7 @@ export class LocConstants {
300301
comment: ["{0} is the IP address of the client"],
301302
}),
302303
addMySubnetRange: l10n.t("Add my subnet IP range"),
304+
ipAddressRange: l10n.t("IP address range"),
303305
fromLabel: l10n.t({
304306
message: "From",
305307
comment: ["Label for the start IP address in the firewall rule IP range"],

0 commit comments

Comments
 (0)