Skip to content

Commit 3c990f4

Browse files
Auth: Initial add connection page redesign (#4042)
* authWebview: isConnectionExists method Adds in a new method for each auth form (eg builder id for CodeWhisperer) which simply returns a boolean of if a connection for that form exists (is known by toolkit). This is being done since we will eventually want to know for each auth form if a connection exists, since checking if an auth form connection is actively connected does not tell us if the connection exists. It may exist but not be active. Signed-off-by: Nikolas Komonen <[email protected]> * refactor: auth webview to use existing auths at start Previously, we only looked at actively connected auths when taking the initial snapshot. Now, we will instead look at all existing auths that the extension is aware of. This will handle edge cases where a connection exists, but is not connected and that does not get considered as an existing connection. Additionally, renames some variables + minor refactor Signed-off-by: Nikolas Komonen <[email protected]> * fix: disabling credentials submit button appropriately There was some old logic the enabled/disabled the submit button. This commit removes it. Additionally, there was a slight refactor to appropriately use the submit button disable logic. What it now does is disables the submit button while the authentication of the credentials is happening. Once a result is returned, regardless of success, we enable the button again. Signed-off-by: Nikolas Komonen <[email protected]> * refactor: Add error messages to state instead of frontend In the credentials auth form we manage error messages in the frontend code. This moves the error message management in to the state class since this is a better design. Signed-off-by: Nikolas Komonen <[email protected]> * remove unused code Signed-off-by: Nikolas Komonen <[email protected]> * refactor: credentials in to state This refactors the credentials logic all in to the state class so that we can eventually extract the state class in to the backend code and properly test it. Signed-off-by: Nikolas Komonen <[email protected]> * refactor: builder id use state Improves how we manage state for Builder ID Signed-off-by: Nikolas Komonen <[email protected]> * refactor: identity center use state more Signed-off-by: Nikolas Komonen <[email protected]> * fix: credentials content panel not loading This was not loading due to a race condition which is described in a comment in the code of this commit. - Additionally, slightly refactor updateIsAllAuthsLoaded() to improve readability. Signed-off-by: Nikolas Komonen <[email protected]> * test/refactor: auth webview code + tests for emitWebviewClosed() Signed-off-by: nkomonen <[email protected]> * refactor: use mapping for auth_addedConnections call When emitting our telemetry we give it a key which consists of values separate from AuthFormId. AuthFormId is good enough and it is better to use that throughout our code than change it up for a different use case. This commit uses AuthFormId for the telemetry method calls, right before telemetry we use a map to convert the AuthFormId in to the required fields for telemetry. Signed-off-by: nkomonen <[email protected]> * tests: add + fix tests for auth_addedConnections Signed-off-by: nkomonen <[email protected]> * refactor: remove unnecessary code Signed-off-by: nkomonen <[email protected]> * refactor: initial auth count is collapsed in telemetry now when we show the count of the initial auths it will be collapsed. eg: not every idc and credential are counted. Instead they are collapsed in to their own count. Signed-off-by: nkomonen <[email protected]> * refactor: notification messages Better architect the notification messages. - Move the definitions of them in to their own components - Create a Model+Controller class to manage them and consolidate all the notification related code in to a single class. TODO: Look to dynamically create notifications so we can simply add a new notification implementation and can call the controller to create it. The difficult part is getting type completion for the different types of notifications since their Props can vary and we'll want a nice way to provide an ID then get the proper type suggestions for the correct args. Signed-off-by: nkomonen <[email protected]> * remove stuff Signed-off-by: nkomonen <[email protected]> * refactor: webview scaling is reactive to width Signed-off-by: nkomonen <[email protected]> * working: before screwing with scaling Signed-off-by: nkomonen <[email protected]> * refactor: sizing/scaling is handled Signed-off-by: nkomonen <[email protected]> * refactor: All feature panels are aligned now Signed-off-by: nkomonen <[email protected]> * refactor: auth changes in backend update frontend This needed to be fixed due to the overall frontend refactor Signed-off-by: nkomonen <[email protected]> * refactor: various fixes Signed-off-by: nkomonen <[email protected]> * feat: open feedback form button in Add Connection page Signed-off-by: nkomonen <[email protected]> * refactor: improve UI refreshing when auth changes - Now refresh the individual feature instead of all of them since we have more granularity in to what changed Signed-off-by: nkomonen <[email protected]> * fix: first time user error on auto open add connection page Since the Auth module was loaded before telemetry, when auth ran it emitted a telemetry event before telemetry was set up. This has telemetry setup first, then auth. Signed-off-by: nkomonen <[email protected]> * fix: circular dependency issues Moves code around to resolve circular dependency issues Signed-off-by: nkomonen <[email protected]> * webview: improve refreshing/loading of auth forms Less stuttery and more isolated Signed-off-by: nkomonen <[email protected]> * changelog item Signed-off-by: nkomonen <[email protected]> * fix unit tests Signed-off-by: nkomonen <[email protected]> * codecatalyst: minor UI fix in add connection page Signed-off-by: nkomonen <[email protected]> * refactor: improve reloading/rendering Will keep the Identity Center forms exposed if they are connected. Signed-off-by: nkomonen <[email protected]> * refactor: add in separate CW manage connections command As to not disturb the downstream private repo I am adding this back in for now Signed-off-by: nkomonen <[email protected]> --------- Signed-off-by: Nikolas Komonen <[email protected]> Signed-off-by: nkomonen <[email protected]>
1 parent 01e7d04 commit 3c990f4

36 files changed

+1812
-1549
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Auth: Redesign Add Connection page to show all options at once"
4+
}

scripts/build/generateIcons.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ async function generate(mappings: Record<string, number | undefined> = {}) {
109109
fontHeight: 1000,
110110
template: 'css',
111111
templateClassName: 'icon',
112+
descent: 200, // Icons were negatively offset on the y-axes, this fixes it
112113
templateFontPath: path.relative(stylesheetsDir, fontsDir),
113114
glyphTransformFn: obj => {
114115
const filePath = (obj as { path?: string }).path

src/auth/secondaryAuth.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,12 @@ export class SecondaryAuth<T extends Connection = Connection> {
105105
) {
106106
await this.clearSavedConnection()
107107
} else {
108+
const currentConn = this.activeConnection
108109
this.#activeConnection = conn
109-
this.#onDidChangeActiveConnection.fire(this.activeConnection)
110+
if (currentConn?.id !== this.activeConnection?.id) {
111+
// The user will get a different active connection from before, so notify them.
112+
this.#onDidChangeActiveConnection.fire(this.activeConnection)
113+
}
110114
}
111115
}
112116

src/auth/ui/vue/authForms/baseAuth.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { defineComponent } from 'vue'
33
import { AuthFormId } from './types'
44
import TelemetryClient from '../telemetry.vue'
5+
import { Notifications } from '../notifications/notifications.vue'
56
67
export type ConnectionUpdateCause = 'signIn' | 'signOut' | 'created'
78
export type ConnectionUpdateArgs = { id: AuthFormId; isConnected: boolean; cause?: ConnectionUpdateCause }
@@ -11,6 +12,7 @@ export default defineComponent({
1112
extends: TelemetryClient,
1213
methods: {
1314
emitAuthConnectionUpdated(args: ConnectionUpdateArgs) {
15+
Notifications.instance.showSuccessNotification(args)
1416
this.$emit('auth-connection-updated', args)
1517
},
1618
},

src/auth/ui/vue/authForms/formTitle.vue

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
-->
55

66
<template>
7-
<div v-if="isConnected" style="display: flex">
8-
<div class="pass-icon icon icon-lg icon-vscode-pass-filled"></div>
9-
<label class="auth-form-title">Connected to <slot></slot></label>
7+
<div v-if="isConnected" style="display: flex; gap: 1em">
8+
<label class="auth-form-title"
9+
><div class="pass-icon icon icon-vscode-pass-filled"></div>
10+
Connected to <slot></slot
11+
></label>
1012
</div>
1113
<div v-else>
1214
<label class="auth-form-title"><slot></slot></label>
@@ -28,6 +30,5 @@ export default defineComponent({
2830
<style>
2931
.pass-icon {
3032
color: #73c991;
31-
margin-right: 5px;
3233
}
3334
</style>

src/auth/ui/vue/authForms/manageBuilderId.vue

Lines changed: 111 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
<template>
2-
<div class="auth-form container-background border-common" id="builder-id-form">
2+
<div class="auth-form container-background border-common">
33
<div>
44
<FormTitle :isConnected="isConnected">AWS Builder ID</FormTitle>
55

66
<div v-if="stage === 'START'">
77
<div class="form-section">
8-
<div class="sub-text-color">
9-
{{ getDescription() }}
8+
<div class="auth-form-description form-description-color">
9+
{{ description }}
1010
<a :href="signUpUrl" v-on:click="emitUiClick('auth_learnMoreBuilderId')">Learn more.</a>
1111
</div>
1212
</div>
1313

1414
<div class="form-section">
1515
<button v-on:click="startSignIn()">{{ submitButtonText }}</button>
16-
<div class="small-description error-text">{{ error }}</div>
16+
<div class="form-description-color input-description-small error-text">{{ error }}</div>
1717
</div>
1818
</div>
1919

@@ -45,6 +45,7 @@ import { WebviewClientFactory } from '../../../../webviews/client'
4545
import { AuthError } from '../types'
4646
import { FeatureId } from '../../../../shared/telemetry/telemetry.gen'
4747
import { AuthForm } from './shared.vue'
48+
import { CredentialSourceId } from '../../../../shared/telemetry/telemetry.gen'
4849
4950
const client = WebviewClientFactory.create<AuthWebview>()
5051
@@ -68,64 +69,45 @@ export default defineComponent({
6869
builderIdCode: '',
6970
name: this.state.name,
7071
error: '' as string,
71-
signUpUrl: '' as string,
72+
signUpUrl: this.state.getSignUpUrl(),
7273
submitButtonText: '' as string,
74+
description: this.state.getDescription(),
7375
}
7476
},
7577
async created() {
76-
this.signUpUrl = this.getSignUpUrl()
77-
await this.update('created')
78+
await this.emitUpdate('created')
7879
},
7980
methods: {
8081
async startSignIn() {
82+
await this.state.startAuthFormInteraction()
83+
84+
// update UI to show a pending state
8185
this.stage = 'WAITING_ON_USER'
82-
client.startAuthFormInteraction(this.state.featureType, 'awsId')
83-
const authError = await this.state.startBuilderIdSetup()
84-
85-
if (authError) {
86-
this.error = authError.text
87-
this.stage = await this.state.stage()
88-
89-
client.failedAuthAttempt({
90-
authType: 'awsId',
91-
featureType: this.state.featureType,
92-
reason: authError.id,
93-
})
86+
87+
const wasSuccessful = await this.state.startBuilderIdSetup()
88+
if (wasSuccessful) {
89+
await this.emitUpdate('signIn')
9490
} else {
95-
client.successfulAuthAttempt({
96-
featureType: this.state.featureType,
97-
authType: 'awsId',
98-
})
99-
await this.update('signIn')
91+
await this.updateForm()
10092
}
10193
},
102-
async update(cause?: ConnectionUpdateCause) {
103-
await this.updateSubmitButtonText()
94+
/** Updates the content of the form using the state data */
95+
async updateForm() {
96+
this.error = this.state.error
10497
this.stage = await this.state.stage()
98+
this.submitButtonText = await this.state.getSubmitButtonText()
10599
this.isConnected = await this.state.isAuthConnected()
100+
},
101+
async emitUpdate(cause?: ConnectionUpdateCause) {
102+
await this.updateForm()
106103
this.emitAuthConnectionUpdated({ id: this.state.id, isConnected: this.isConnected, cause })
107104
},
108105
async signout() {
109106
await this.state.signout()
110-
client.emitUiClick(this.state.uiClickSignout)
111-
this.update('signOut')
107+
this.emitUpdate('signOut')
112108
},
113109
showNodeInView() {
114110
this.state.showNodeInView()
115-
client.emitUiClick(this.state.uiClickOpenId)
116-
},
117-
getSignUpUrl() {
118-
return this.state.getSignUpUrl()
119-
},
120-
getDescription() {
121-
return this.state.getDescription()
122-
},
123-
async updateSubmitButtonText() {
124-
if (!(await isBuilderIdConnected())) {
125-
this.submitButtonText = 'Sign up or Sign in'
126-
} else {
127-
this.submitButtonText = `Connect AWS Builder ID with ${this.state.name}`
128-
}
129111
},
130112
},
131113
})
@@ -135,6 +117,7 @@ export default defineComponent({
135117
*/
136118
abstract class BaseBuilderIdState implements AuthForm {
137119
protected _stage: BuilderIdStage = 'START'
120+
#error: string = ''
138121
139122
abstract get name(): string
140123
abstract get id(): AuthFormId
@@ -143,13 +126,30 @@ abstract class BaseBuilderIdState implements AuthForm {
143126
abstract get featureType(): FeatureId
144127
protected abstract _startBuilderIdSetup(): Promise<AuthError | undefined>
145128
abstract isAuthConnected(): Promise<boolean>
146-
abstract showNodeInView(): Promise<void>
147-
148-
protected constructor() {}
129+
abstract _showNodeInView(): Promise<void>
130+
abstract isConnectionExists(): Promise<boolean>
131+
132+
/**
133+
* Starts the Builder ID setup.
134+
*
135+
* Returns true if was successful.
136+
*/
137+
async startBuilderIdSetup(): Promise<boolean> {
138+
this.#error = ''
139+
140+
const authError = await this._startBuilderIdSetup()
141+
142+
if (authError) {
143+
this.#error = authError.text
144+
client.failedAuthAttempt(this.id, {
145+
reason: authError.id,
146+
})
147+
} else {
148+
this.#error = ''
149+
client.successfulAuthAttempt(this.id)
150+
}
149151
150-
async startBuilderIdSetup(): Promise<AuthError | undefined> {
151-
this._stage = 'WAITING_ON_USER'
152-
return this._startBuilderIdSetup()
152+
return authError === undefined
153153
}
154154
155155
async stage(): Promise<BuilderIdStage> {
@@ -160,6 +160,40 @@ abstract class BaseBuilderIdState implements AuthForm {
160160
161161
async signout(): Promise<void> {
162162
await client.signoutBuilderId()
163+
client.emitUiClick(this.uiClickSignout)
164+
}
165+
166+
get authType(): CredentialSourceId {
167+
return 'awsId'
168+
}
169+
170+
get error(): string {
171+
return this.#error
172+
}
173+
174+
/**
175+
* In the scenario a Builder ID is already connected,
176+
* we want to change the submit button text for all unconnected
177+
* Builder IDs to something else since they are not techincally
178+
* signing in again, but instead adding scopes.
179+
*/
180+
async getSubmitButtonText(): Promise<string> {
181+
if (!(await this.anyBuilderIdConnected())) {
182+
return 'Sign up or Sign in'
183+
} else {
184+
return `Connect AWS Builder ID with ${this.name}`
185+
}
186+
}
187+
188+
/**
189+
* Returns true if any Builder Id is connected
190+
*/
191+
private async anyBuilderIdConnected(): Promise<boolean> {
192+
const results = await Promise.all([
193+
CodeWhispererBuilderIdState.instance.isAuthConnected(),
194+
CodeCatalystBuilderIdState.instance.isAuthConnected(),
195+
])
196+
return results.some(isConnected => isConnected)
163197
}
164198
165199
getSignUpUrl(): string {
@@ -169,6 +203,15 @@ abstract class BaseBuilderIdState implements AuthForm {
169203
getDescription(): string {
170204
return 'With AWS Builder ID, sign in for free without an AWS account.'
171205
}
206+
207+
startAuthFormInteraction() {
208+
return client.startAuthFormInteraction(this.featureType, this.authType)
209+
}
210+
211+
showNodeInView() {
212+
this._showNodeInView()
213+
client.emitUiClick(this.uiClickOpenId)
214+
}
172215
}
173216
174217
export class CodeWhispererBuilderIdState extends BaseBuilderIdState {
@@ -196,20 +239,26 @@ export class CodeWhispererBuilderIdState extends BaseBuilderIdState {
196239
return client.isCodeWhispererBuilderIdConnected()
197240
}
198241
242+
override isConnectionExists(): Promise<boolean> {
243+
return client.hasBuilderId('codewhisperer')
244+
}
245+
199246
protected override _startBuilderIdSetup(): Promise<AuthError | undefined> {
200247
return client.startCodeWhispererBuilderIdSetup()
201248
}
202249
203-
override showNodeInView(): Promise<void> {
250+
override _showNodeInView(): Promise<void> {
204251
return client.showCodeWhispererNode()
205252
}
206253
207254
override getSignUpUrl(): string {
208255
return 'https://docs.aws.amazon.com/codewhisperer/latest/userguide/whisper-setup-indv-devs.html'
209256
}
210257
258+
private constructor() {
259+
super()
260+
}
211261
static #instance: CodeWhispererBuilderIdState | undefined
212-
213262
static get instance(): CodeWhispererBuilderIdState {
214263
return (this.#instance ??= new CodeWhispererBuilderIdState())
215264
}
@@ -240,46 +289,36 @@ export class CodeCatalystBuilderIdState extends BaseBuilderIdState {
240289
return client.isCodeCatalystBuilderIdConnected()
241290
}
242291
292+
override isConnectionExists(): Promise<boolean> {
293+
return client.hasBuilderId('codecatalyst')
294+
}
295+
243296
protected override _startBuilderIdSetup(): Promise<AuthError | undefined> {
244297
return client.startCodeCatalystBuilderIdSetup()
245298
}
246299
247-
override showNodeInView(): Promise<void> {
300+
override _showNodeInView(): Promise<void> {
248301
return client.showCodeCatalystNode()
249302
}
250303
251-
static #instance: CodeCatalystBuilderIdState | undefined
252-
253-
static get instance(): CodeCatalystBuilderIdState {
254-
return (this.#instance ??= new CodeCatalystBuilderIdState())
255-
}
256-
257304
override getDescription(): string {
258305
return 'You must have an existing CodeCatalyst Space connected to your AWS Builder ID.'
259306
}
260307
261308
override getSignUpUrl(): string {
262309
return 'https://aws.amazon.com/codecatalyst/'
263310
}
264-
}
265311
266-
/**
267-
* Returns true if any Builder Id is connected
268-
*/
269-
export async function isBuilderIdConnected(): Promise<boolean> {
270-
const results = await Promise.all([
271-
CodeWhispererBuilderIdState.instance.isAuthConnected(),
272-
CodeCatalystBuilderIdState.instance.isAuthConnected(),
273-
])
274-
return results.some(isConnected => isConnected)
312+
private constructor() {
313+
super()
314+
}
315+
static #instance: CodeCatalystBuilderIdState | undefined
316+
static get instance(): CodeCatalystBuilderIdState {
317+
return (this.#instance ??= new CodeCatalystBuilderIdState())
318+
}
275319
}
276320
</script>
277321
<style>
278322
@import './sharedAuthForms.css';
279323
@import '../shared.css';
280-
281-
#builder-id-form {
282-
width: 300px;
283-
height: fit-content;
284-
}
285324
</style>

0 commit comments

Comments
 (0)