Skip to content

Commit d370fdb

Browse files
auth: Webview Create Clickable Items + State Management (#3345)
Create Clickable Items + State Management - Creates all the buttons on the left side of the page - Creates a class that manages the state of the buttons This includes: - The item that is currently selected by the user - The status of the item (locked, unlocked) - Which section the item should be in the view (locked/unlocked section) Signed-off-by: Nikolas Komonen <[email protected]>
1 parent c2c1df3 commit d370fdb

File tree

2 files changed

+358
-8
lines changed

2 files changed

+358
-8
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<!--
2+
This module focuses on the clickable box that represents a specific service/feature
3+
on the left side of the screen. It defines the base structure of the component and
4+
from there specific service item components can be defined.
5+
6+
Additionaly, this module provides a state manager to keep track of the state of
7+
of the service items.
8+
-->
9+
<template>
10+
<button class="service-item-container" :class="classWhenIsSelected" v-on:mousedown="serviceItemClicked">
11+
<!-- The icon -->
12+
<div class="icon-item" :class="serviceIconClass"></div>
13+
14+
<!-- The text info -->
15+
<div class="text-info-container">
16+
<div class="service-item-title">
17+
{{ title }}
18+
</div>
19+
<div class="service-item-description">
20+
{{ description }}
21+
</div>
22+
</div>
23+
</button>
24+
</template>
25+
<script lang="ts">
26+
import { defineComponent, PropType } from 'vue'
27+
28+
/* The status of the icon for a service */
29+
type ServiceIconStatus = keyof typeof serviceIconClasses
30+
31+
/* The general status of the service */
32+
export type ServiceStatus = Exclude<ServiceIconStatus, 'LOCKED_SELECTED'>
33+
34+
/**
35+
* Maps a service status to the CSS classes that will create the icon.
36+
*
37+
* LOCKED_SELECTED is a case where the item is locked but selected by the user.
38+
*/
39+
const serviceIconClasses = {
40+
LOCKED: 'icon icon-lg icon-vscode-lock',
41+
LOCKED_SELECTED: 'icon icon-lg icon-vscode-lock locked-selected',
42+
UNLOCKED: 'icon icon-vscode-check unlocked',
43+
} as const
44+
45+
/**
46+
* The static props that are expected to be passed to a ServiceItem component.
47+
*
48+
* Static here implies that these props are not expected to change after the component is created.
49+
*/
50+
export interface StaticServiceItemProps {
51+
title: string
52+
description: string
53+
}
54+
55+
/**
56+
* The base component for a service item that should be extended
57+
* by specific service item components.
58+
*/
59+
export default defineComponent({
60+
name: 'ServiceItem',
61+
props: {
62+
id: {
63+
type: String as PropType<ServiceItemId>,
64+
required: true,
65+
},
66+
title: {
67+
type: String,
68+
required: true,
69+
},
70+
description: {
71+
type: String,
72+
required: true,
73+
},
74+
status: {
75+
type: String as PropType<ServiceStatus>,
76+
default: 'LOCKED',
77+
},
78+
isSelected: {
79+
type: Boolean,
80+
default: false,
81+
},
82+
},
83+
data() {
84+
return {
85+
classWhenIsSelected: '',
86+
serviceIconClasses: serviceIconClasses,
87+
serviceIconClass: '',
88+
}
89+
},
90+
created() {
91+
// The CSS class that should be applied to the container when the item is selected.
92+
this.classWhenIsSelected = this.isSelected ? 'service-item-container-selected' : ''
93+
94+
// The CSS class that determines which icon to show.
95+
const serviceIconStatus: ServiceIconStatus =
96+
this.isSelected && this.status === 'LOCKED' ? 'LOCKED_SELECTED' : this.status
97+
this.serviceIconClass = this.serviceIconClasses[serviceIconStatus]
98+
},
99+
methods: {
100+
serviceItemClicked() {
101+
this.$emit('service-item-clicked', this.id)
102+
},
103+
},
104+
})
105+
106+
/**
107+
* ------------------- Service Item Implementations -------------------
108+
*
109+
* All specific service item components should be defined below.
110+
*/
111+
112+
/**
113+
* A Service Item ID is the main identifier/representation of a specific service item.
114+
*/
115+
export type ServiceItemId = keyof typeof staticServiceItemProps
116+
117+
const staticServiceItemProps = {
118+
NON_AUTH_FEATURES: {
119+
title: 'Debug Lambda Functions & Edit AWS Document Types',
120+
description: "Local features that don't require authentication",
121+
},
122+
RESOURCE_EXPLORER: {
123+
title: 'Resource Explorer',
124+
description: 'View, modify, deploy, and troubleshoot AWS resources',
125+
},
126+
CODE_WHISPERER: {
127+
title: 'Amazon CodeWhisperer',
128+
description: 'Build applications faster with AI code recommendations',
129+
},
130+
CODE_CATALYST: {
131+
title: 'Amazon CodeCatalyst',
132+
description: 'Spark a faster planning, development, and delivery lifecycle on AWS',
133+
},
134+
} as const
135+
136+
/* -------------------------------------- */
137+
138+
/**
139+
* This class is responsible for keeping track of the state of all service items.
140+
*
141+
* As the user interacts with the service items, certain methods of this class
142+
* can be used to update the state of specific service items. Then, the method
143+
* {@link getServiceIds} can be used to get the latest state of all service items.
144+
*/
145+
export class ServiceItemsState {
146+
/**
147+
* IDs of all services that are currently unlocked
148+
*
149+
* Note the default unlocked service(s) are pre-defined here.
150+
*/
151+
private readonly unlockedServices: Set<ServiceItemId> = new Set(['NON_AUTH_FEATURES'])
152+
153+
/** Note a service item is pre-selected by default */
154+
private currentlySelected: ServiceItemId = 'NON_AUTH_FEATURES'
155+
156+
/**
157+
* The Ids of the service items, separated by the ones that are locked vs. unlocked
158+
*
159+
* IMPORTANT: This is the source of truth of the current state of all service items.
160+
* Use the methods of this class to modify the states of items, then use
161+
* this method to get the latest state.
162+
*/
163+
getServiceIds(): { unlocked: ServiceItemId[]; locked: ServiceItemId[] } {
164+
const allServiceIds = Object.keys(staticServiceItemProps) as ServiceItemId[]
165+
const unlockedConstructorIds = allServiceIds.filter(id => this.unlockedServices.has(id))
166+
const lockedConstructorIds = allServiceIds.filter(id => !this.unlockedServices.has(id))
167+
168+
return {
169+
unlocked: unlockedConstructorIds,
170+
locked: lockedConstructorIds,
171+
}
172+
}
173+
174+
/**
175+
* Static Service Item props are the props that are not expected to change
176+
* after the component is created.
177+
*/
178+
getStaticServiceItemProps(id: ServiceItemId): StaticServiceItemProps {
179+
return staticServiceItemProps[id]
180+
}
181+
182+
/** The currently selected service item */
183+
get selected(): ServiceItemId {
184+
return this.currentlySelected
185+
}
186+
187+
/** Marks the item as selected by the user */
188+
select(id: ServiceItemId) {
189+
this.currentlySelected = id
190+
}
191+
192+
/** Marks the item as being 'unlocked', implying the required auth is completed. */
193+
unlock(id: ServiceItemId) {
194+
this.unlockedServices.add(id)
195+
}
196+
197+
/** Marks the item as being 'locked', implying the required auth is NOT completed. */
198+
lock(id: ServiceItemId) {
199+
this.unlockedServices.delete(id)
200+
}
201+
}
202+
</script>
203+
204+
<style>
205+
/* ******** Container ******** */
206+
207+
.service-item-container {
208+
background-color: #292929;
209+
display: flex;
210+
margin-top: 10px;
211+
padding: 20px 15px 20px 15px;
212+
width: 100%;
213+
214+
border-style: solid;
215+
border-width: 2px;
216+
border-radius: 4px;
217+
border-color: transparent;
218+
219+
/* Assumes that description is a single line.
220+
If it can be multiple lines this value should be re-evaluated */
221+
height: 75px;
222+
223+
/* Icon and text are centered on the secondary axis */
224+
align-items: center;
225+
}
226+
227+
/* When a service item was clicked */
228+
.service-item-container-selected {
229+
background-color: #3c3c3c;
230+
border-color: #0097fb;
231+
}
232+
233+
/* ******** Icon ******** */
234+
.icon-item {
235+
/* Separation between icon and text */
236+
margin-right: 15px;
237+
}
238+
239+
/* The checkmark symbol */
240+
.unlocked {
241+
color: #73c991;
242+
}
243+
244+
/* The lock symbol but the user has clicked it */
245+
.locked-selected {
246+
color: #0097fb;
247+
}
248+
249+
/* ******** Text ******** */
250+
251+
.service-item-title {
252+
color: #ffffff;
253+
font-size: 13px;
254+
font-weight: 800;
255+
font-family: 'Verdana';
256+
line-height: 16px;
257+
margin-bottom: 5px;
258+
margin-top: 0;
259+
}
260+
261+
.service-item-description {
262+
color: #cccccc;
263+
font-size: 12px;
264+
font-weight: 500;
265+
font-family: 'Verdana';
266+
line-height: 14px;
267+
margin-bottom: 0;
268+
margin-top: 0;
269+
}
270+
271+
.text-info-container {
272+
display: flex;
273+
flex-direction: column;
274+
text-align: left;
275+
}
276+
</style>

src/credentials/vue/root.vue

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,92 @@
11
<template>
22
<div class="flex-container" style="display: flex">
3-
<div id="left-column">I am left column</div>
4-
<div id="right-column">I am right column</div>
3+
<div id="left-column">
4+
<div>
5+
<h1>Select a feature to get started</h1>
6+
<div v-for="itemId in unlockedItemIds">
7+
<ServiceItem
8+
:title="getServiceItemProps(itemId).title"
9+
:description="getServiceItemProps(itemId).description"
10+
:status="'UNLOCKED'"
11+
:isSelected="isServiceSelected(itemId)"
12+
:id="itemId"
13+
:key="buildServiceItemKey(itemId, 'UNLOCKED')"
14+
@service-item-clicked="serviceWasSelected(itemId)"
15+
>
16+
</ServiceItem>
17+
</div>
18+
</div>
19+
20+
<div>
21+
<h3>UNLOCK ADDITIONAL FEATURES</h3>
22+
<div>Some features have additional authentication requirements to use. <a>Read more.</a></div>
23+
24+
<div v-for="itemId in lockedItemIds">
25+
<ServiceItem
26+
:title="getServiceItemProps(itemId).title"
27+
:description="getServiceItemProps(itemId).description"
28+
:status="'LOCKED'"
29+
:isSelected="isServiceSelected(itemId)"
30+
:id="itemId"
31+
:key="buildServiceItemKey(itemId, 'LOCKED')"
32+
@service-item-clicked="serviceWasSelected(itemId)"
33+
>
34+
</ServiceItem>
35+
</div>
36+
</div>
37+
<h3></h3>
38+
</div>
39+
<div id="right-column"></div>
540
</div>
641
</template>
742

843
<script lang="ts">
944
import { defineComponent } from 'vue'
45+
import ServiceItem, { ServiceItemsState, ServiceItemId, ServiceStatus, StaticServiceItemProps } from './ServiceItem.vue'
46+
47+
const serviceItemsState = new ServiceItemsState()
1048
1149
export default defineComponent({
12-
name: 'MyComponent',
50+
components: { ServiceItem },
51+
name: 'AuthRoot',
52+
data() {
53+
return {
54+
unlockedItemIds: [] as ServiceItemId[],
55+
lockedItemIds: [] as ServiceItemId[],
56+
}
57+
},
58+
created() {
59+
this.renderItems()
60+
},
61+
methods: {
62+
/**
63+
* Triggers a rendering of the service items.
64+
*/
65+
renderItems() {
66+
const { unlocked, locked } = serviceItemsState.getServiceIds()
67+
this.unlockedItemIds = unlocked
68+
this.lockedItemIds = locked
69+
},
70+
isServiceSelected(id: ServiceItemId): boolean {
71+
return serviceItemsState.selected === id
72+
},
73+
getServiceItemProps(id: ServiceItemId): StaticServiceItemProps {
74+
return serviceItemsState.getStaticServiceItemProps(id)
75+
},
76+
serviceWasSelected(id: ServiceItemId): void {
77+
serviceItemsState.select(id)
78+
this.renderItems()
79+
},
80+
/**
81+
* Builds a unique key for a service item to optimize re-rendering.
82+
*
83+
* This allows Vue to know which existing component to compare to the new one.
84+
* https://vuejs.org/api/built-in-special-attributes.html#key
85+
*/
86+
buildServiceItemKey(id: ServiceItemId, lockStatus: ServiceStatus) {
87+
return id + '_' + (this.isServiceSelected(id) ? `${lockStatus}_SELECTED` : `${lockStatus}`)
88+
},
89+
},
1390
})
1491
</script>
1592

@@ -33,11 +110,8 @@ export default defineComponent({
33110
#left-column {
34111
flex-grow: 1;
35112
max-width: 40%;
36-
37-
/* This can be deleted, for development purposes */
38-
background-color: yellow;
39-
color: black;
40-
height: 200px;
113+
box-sizing: border-box;
114+
margin: 10px;
41115
}
42116
43117
#right-column {

0 commit comments

Comments
 (0)