Skip to content

Commit 5b01036

Browse files
authored
refactor(webview): improve ergonomics (#2644)
1 parent fc6f903 commit 5b01036

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1403
-1976
lines changed

.prettierignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,3 @@ CHANGELOG.md
55
src/shared/telemetry/service-2.json
66
.changes
77
src/testFixtures/**
8-
resources/markdown/samReadme.md

docs/ARCHITECTURE.md

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ Be very mindful about state managment; violating these principles will lead to b
1717

1818
### Bundling
1919

20-
Each webview is bundled into a single file to speed up load times as well as isolate the 'web' modules from the 'node' modules. Webview bundles are automatically generated on compilation by targeting `entry.ts` files when located under a `vue` directory. All bundles are placed directly under `dist`.
20+
Each webview is bundled into a single file to speed up load times as well as isolate the 'web' modules from the 'node' modules. Webview bundles are automatically generated on compilation by targeting `index.ts` files when located under a `vue` directory. All bundles are placed under `dist` in the same relative location.
2121

22-
Generated bundle names map based off their path relative to `src`: `src/foo/vue/bar/entry.ts` -> `fooBarVue.js`
22+
Generated bundle names map based off their path relative to `src`: `src/foo/vue/bar/index.ts` -> `dist/src/foo/vue/bar/index.js`
2323

2424
Running the extension in development mode (e.g. via the `Extension` launch task) starts a local server to automatically rebuild and serve webviews in real-time via hot-module reloading. It's assumed that the server runs on port `8080`, so make sure that nothing is already using that port. Otherwise webviews will not be displayed during development.
2525

@@ -29,37 +29,41 @@ The VS Code API restricts our Webviews to a single `postMessage` function. To si
2929

3030
Webview (frontend) clients can be created via `WebviewClientFactory`. This generates a simple Proxy to send messages to the extension, mapping the function name to the command name. Unique IDs are also generated to stop requests from receiving extraneous responses. It is **highly** recommended to use the [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) extension for syntax highlighting and type-checking when working with SFCs. Keep in mind that this is purely a development tool: users of the toolkits do not require Volar to view webviews.
3131

32-
Commands and events are defined on the backend via `compileVueWebview` or `compileVueWebviewView` for the special 'view' case. This takes a configuration object that contains information about the webview, such as the name of the main script, the panel id, and the commands/events that the backend provides. This returns a class that can be instantiated into the webview. Webviews can then be executed by calling `start` with any initial data (if applicable). Webviews can be cleared of their internal state without reloading the HTML by calling `clear` with any re-initialization data (if applicable).
32+
Commands and events are defined on the backend via sub-classes of `VueWebview`. Classes define all the functions and events that will be available to the frontend. These sub-classes can be turned into a fully-resolved class by using either `VueWeview.compilePanel` or `VueWebview.compileView`. The resulting classes can finally be used to instantiate webview panels or view. Panels are shown by calling `show`, while views must be registered before they can be seen.
3333

3434
### Examples
3535

3636
- Creating and executing a webview:
3737

3838
```ts
39-
const VueWebview = compileVueWebview({
40-
id: 'my.view',
41-
title: 'A title',
42-
webviewJs: 'myView.js',
43-
start: (param?: string) => param ?? 'foo',
44-
events: {
45-
onBar: new vscode.EventEmitter<number>(),
46-
},
47-
commands: {
48-
foo: () => 'hello!',
49-
bar() {
50-
// All commands have access to `WebviewServer` via `this`
51-
this.emiters.onBar.fire(0)
52-
},
53-
},
54-
})
39+
// Export the class so the frontend code can use it for types
40+
export class MyVueWebview extends VueWebview {
41+
public readonly id = 'my.view'
42+
public readonly source = 'myView.js'
43+
public readonly onBar = new vscode.EventEmitter<number>()
5544

56-
// `context` is `ExtContext` provided on activation
57-
const view = new VueWebview(context)
58-
view.start('some data')
59-
view.emitters.onFoo.fire(1)
45+
public constructor(private readonly myData: string) {}
46+
47+
public init() {
48+
return this.myData
49+
}
6050

61-
// Export a class so the frontend code can use it for types
62-
export class MyView extends VueWebview {}
51+
public foo() {
52+
return 'foo'
53+
}
54+
55+
public bar() {
56+
this.onBar.fire(0)
57+
}
58+
}
59+
60+
// Create panel bindings using our class
61+
const Panel = VueWebview.compilePanel(MyVueWebview)
62+
63+
// `context` is `ExtContext` provided on activation
64+
const view = new Panel(context, 'hello')
65+
view.show({ title: 'My title' })
66+
view.server.onFoo.fire(1)
6367
```
6468

6569
- Creating the client on the frontend:
@@ -86,22 +90,12 @@ Commands and events are defined on the backend via `compileVueWebview` or `compi
8690
client.onBar(num => console.log(num))
8791
```
8892

89-
- Retrieving initialization data by calling the `init` method:
93+
- Methods called `init` will only return data on the initial webview load:
9094

9195
```ts
92-
client.init(data => console.log(data))
96+
client.init(data => (this.data = data ?? this.data))
9397
```
9498

95-
Note that data is retrieved only **once**. Subsequent calls made by the same webview resolve `undefined` unless the state is cleared either by `clear` or refreshing the view.
96-
97-
- Submitting a result (this destroys the view on success):
98-
99-
```ts
100-
client.submit(result).catch(err => console.error('Something went wrong!'))
101-
```
102-
103-
`submit` does nothing on views registered as a `WebviewView` as they cannot be disposed of by the extension.
104-
10599
### Testing
106100

107101
Currently only manual testing is done. Future work will include setting up some basic unit testing capacity via `JSDOM` and `Vue Testing Library`. Strict type-checking may also be enforced on SFCs; currently the type-checking only exists locally due to gaps in the type definitions for the DOM provided by Vue/TypeScript.

resources/markdown/samReadme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ ${COMPANYNAME} Lambda functions not defined in the [`template.yaml`](./template.
1919

2020
## Deploying Serverless Applications
2121

22-
You can deploy a serverless application by invoking the `AWS: Deploy SAM application` command through the Command Palette or by right-clicking the Lambda node in the ${COMPANYNAME} Explorer and entering the deployment region, a valid S3 bucket from the region, and the name of a CloudFormation stack to deploy to. You can monitor your deployment's progress through the **${COMPANYNAME} Toolkit** Output Channel.
22+
You can deploy a serverless application by invoking the `AWS: Deploy SAM application` command through the Command Palette or by right-clicking the Lambda node in the ${COMPANYNAME} Explorer and entering the deployment region, a valid S3 bucket from the region, and the name of a CloudFormation stack to deploy to. You can monitor your deployment's progress through the `${COMPANYNAME} Toolkit` Output Channel.
2323

2424
## Interacting With Deployed Serverless Applications
2525

src/apigateway/activation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { RestApiNode } from './explorer/apiNodes'
8-
import { invokeRemoteRestApi } from './commands/invokeRemoteRestApi'
8+
import { invokeRemoteRestApi } from './vue/invokeRemoteRestApi'
99
import { copyUrlCommand } from './commands/copyUrl'
1010
import { ExtContext } from '../shared/extensions'
1111

File renamed without changes.

src/apigateway/vue/invokeApi.vue

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
<br />
3535
<textarea rows="20" cols="90" v-model="jsonInput"></textarea>
3636
<br />
37-
<input type="submit" v-on:click="sendInput" value="Invoke" :disabled="isLoading" />
37+
<button class="mt-16 mb-16" @click="sendInput" :disabled="isLoading">{{ invokeText }}</button>
3838
<br />
3939
<div v-if="errors.length">
4040
<b>Please correct the following error(s):</b>
@@ -49,7 +49,7 @@
4949
import { defineComponent } from 'vue'
5050
import { WebviewClientFactory } from '../../webviews/client'
5151
import saveData from '../../webviews/mixins/saveData'
52-
import { InvokeRemoteRestApiInitialData, RemoteRestInvokeWebview } from '../commands/invokeRemoteRestApi'
52+
import { InvokeRemoteRestApiInitialData, RemoteRestInvokeWebview } from './invokeRemoteRestApi'
5353
5454
const client = WebviewClientFactory.create<RemoteRestInvokeWebview>()
5555
@@ -71,7 +71,7 @@ export default defineComponent({
7171
initialData: defaultInitialData,
7272
selectedApiResource: '',
7373
selectedMethod: '',
74-
methods: [],
74+
methods: [] as string[],
7575
jsonInput: '',
7676
queryString: '',
7777
errors: [] as string[],
@@ -80,35 +80,11 @@ export default defineComponent({
8080
async created() {
8181
this.initialData = (await client.init()) ?? this.initialData
8282
},
83-
mounted() {
84-
this.$nextTick(function () {
85-
window.addEventListener('message', this.handleMessageReceived)
86-
})
87-
},
8883
methods: {
89-
handleMessageReceived: function (event: any) {
90-
const message = event.data
91-
switch (message.command) {
92-
case 'setMethods':
93-
this.methods = message.methods
94-
if (this.methods) {
95-
this.selectedMethod = this.methods[0]
96-
}
97-
break
98-
case 'invokeApiStarted':
99-
this.isLoading = true
100-
break
101-
case 'invokeApiFinished':
102-
this.isLoading = false
103-
break
104-
}
105-
},
106-
setApiResource: function () {
107-
client.handler({
108-
command: 'apiResourceSelected',
109-
resource: this.initialData.Resources[this.selectedApiResource],
110-
region: this.initialData.Region,
111-
})
84+
setApiResource: async function () {
85+
const methods = await client.listValidMethods(this.initialData.Resources[this.selectedApiResource])
86+
this.methods = methods
87+
this.selectedMethod = methods[0]
11288
},
11389
sendInput: function () {
11490
this.errors = []
@@ -122,15 +98,22 @@ export default defineComponent({
12298
return
12399
}
124100
125-
client.handler({
126-
command: 'invokeApi',
127-
body: this.jsonInput,
128-
api: this.initialData.ApiId,
129-
selectedApiResource: this.initialData.Resources[this.selectedApiResource],
130-
selectedMethod: this.selectedMethod,
131-
queryString: this.queryString,
132-
region: this.initialData.Region,
133-
})
101+
this.isLoading = true
102+
client
103+
.invokeApi({
104+
body: this.jsonInput,
105+
api: this.initialData.ApiId,
106+
selectedApiResource: this.initialData.Resources[this.selectedApiResource],
107+
selectedMethod: this.selectedMethod,
108+
queryString: this.queryString,
109+
region: this.initialData.Region,
110+
})
111+
.finally(() => (this.isLoading = false))
112+
},
113+
},
114+
computed: {
115+
invokeText() {
116+
return this.isLoading ? 'Invoking...' : 'Invoke'
134117
},
135118
},
136119
})

0 commit comments

Comments
 (0)