Skip to content

Commit 64380ab

Browse files
Merge master into feature/web
2 parents 5cd2ccd + 75da796 commit 64380ab

40 files changed

+1936
-1561
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "Fix a not connected error when starting connection to CodeCatalyst Dev Environment from link"
4+
}
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)