Closed ainvoner closed 1 year ago
The only difference between both options is the layout
being in BaseMeta? Anyway, I think it should be implemented in a slightly different way.
I agree in that we need a type: string
, but it should be extendable in userland as well. Because of that, we should name our base types with something like io.winglang.input
, or cloud.wing.input
, or whatever. Userland types will use their own domains for their own types. Not sure how userland types will be implemented in the Console, yet.
Now, I don't think it makes sense to carry the layout
property everywhere. I believe Option 2 is the better option, since it's flexible and not hard at all to use IMO.
For starters, I'd like to restrict it to a list of Group[]
, and remove the Layout type. So, a resource would only provide a list of groups with the other items in it.
The communication between the Console and the Simulator goes both ways (eg, a button will do something inside a resource). I think our meta should allow to be used in the context of a long-running router, like this:
// sim/reverse-proxy.ts
...
...
Node.of(this).display.meta = {
"type": "group",
"label": "urls",
"layout": "vertical"
"components": [
{
"type": "link",
// Can be accessed anytime.
"href": () => this.urls[0],
"label": "main url",
},
{
"type": "input",
"value": () => this.urls[1],
onEdit: (value) => {
// do something. this is inflight.
},
"label": "secondary url"
}
]
}
Finally, instead of having the developers to use this format directly, we should offer a small SDK that can construct it instead. For example:
Node.of(this).display.meta = [
meta.group({
"label": "urls",
"components": [
meta.link({
// Can be accessed anytime.
"href": () => this.urls[0],
"label": "main url",
}),
meta.input({
"value": () => this.urls[1],
onEdit: (value) => {
// do something. this is inflight.
},
"label": "secondary url",
}),
],
}),
];
Before deciding anything, I'd suggest we read how Laravel Nova works: it allows developers to provide an admin panel for the Laravel PHP backend. It's the same concept as the Console for the simulated resources.
@skyrpex I don't really follow this part:
I agree in that we need a type: string, but it should be extendable in userland as well
I think we should define the building blocks as part of our design system. Do you think others should be able to use their UI components as well? How can we guarantee it will look good in the Console?
I do agree that a variation of Option 2 would be better
react - json standards: https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema/ https://github.com/rjsf-team/react-jsonschema-form https://www.npmjs.com/package/@rjsf/core
some references: https://developer.android.com/jetpack/compose/layouts/basics https://nova.laravel.com/docs/4.0/resources/ https://github.com/facebook/yoga
I do feel that the interfaces should be defined according to our design system components.
For example:
Design System component: Link = (label, url) => {<Link Component>}
Wing SDK: Display.addLink(label, url);
Compile to sim target: we can follow uiSchema or create something simpler.
Defining a more complex Layout and Group components compositions can be done in the future (I don't see any need at the moment)
I agree. Maybe for complex UIs such as the File Manager component for Buckets, we could use a very specific component for now, and then think later if we want to define more specific components such as Layouts, Groups, etc.
BTW I'll have a look at the uiSchema
Edit: I'd prefer to start simple, without uiSchema, something similar to your Option 2.
Sharing some ideas from a conversation I had with @skyrpex:
I feel like a lot of users might want to have a way to configure resource interactions without specifying how they're displayed (like layouts or formatting). For example, maybe it could be handy to be able to define some "quick actions" and labeled information for my resource that users can interact with in the Wing Console, without the library author needing to think about "will this information fit?" or "what order should the information be laid out?". Here's a sketch of what a high-level API could look like in Wing.
For example in my made-up Widget service, I might want to configure some simple actions and fields like this:
class WidgetService {
init() {
// ...
this.display.addAction(kind: Action.Button, fn: this.redriveFailures);
this.display.addAction(kind: Action.Form, fn: this.makeWidget);
this.display.addField(label: "Widgets produced in the last hour", getter: this.getWidgets, refreshEvery: 5s);
// future (P2)
this.display.addReactView("./widget-interaction.tsx"); // ???
}
inflight redriveFailures() {
// ...
}
inflight makeWidget(name: str) {
// ...
}
inflight getWidgets(): str {
// ...
}
}
The Wing Console has access to the simulator, so it could be pretty straightforward to integrate the logic there. For example, the list of actions and fields would be stored in simulator.json or tree.json, and the Wing Console will access it via a set of APIs, e.g.
simulator.listResourceInteractions("root/Stack1/WidgetService")
simulator.describeResourceInteraction("root/Stack1/WidgetService", "makeWidget")
simulator.runResourceInteraction("root/Stack1/WidgetService", "makeWidget", options)
Maybe an API like this could offer a lot of flexibility for redesigning the Wing Console UI in the future, while simplifying the experience for Wing users. Anyway these are just some ideas I wanted to put out there, excited to see what y'all come up with 👍
This is what I came up with: A new IDisplayableResource interface:
export interface IDisplayableResource {
/**
* Returns a visual model for this resource.
* @returns a visual model for this resource.
* @inflight
*/
visualModel(): Promise<VisualModel | undefined>;
}
That every resource can implement. For example
website.inflight.ts
export class Website
implements IWebsiteClient, ISimulatorResourceInstance, IDisplayableResource
{
...
...
public async visualModel(): Promise<VisualModel | undefined> {
const builder = new VisualModelBuilder();
builder.addLink(this.url, "Cool Website URL");
return builder.buildVisualModel();
}
In the console, for each resource we will be able to get its' visual model and display whatever in its' interaction panel.
const simulator = await ctx.simulator();
const client = simulator.getResource(
input.resourcePath,
) as IDisplayableResource;
if (!client) {
return;
}
return await client.visualModel();
Now, this is working great when added to an sdk sim resources (full E2E).
The problems start when trying to create a custom resource using winglang in a different repository.
Let's take the following custom resource for example:
main.w
class MyWebsite {
w: cloud.Website;
init() {
this.w = new cloud.Website(path: "path/to/website");
}
pub inflight visualModel(): std.VisualModel {
let model = new std.VisualModelBuilder();
model.addLink(this.w.url, "Google");
return model.buildVisualModel();
}
}
Since the Simulator doesn't know this resource and doesn't create a client for it - there is no way to call any of its' inflight methods...
Do I also need to implement ISimulatorResource
and add the following methods for each custom resource? (doesn't make sense obviously....)
public bind(host: IInflightHost, ops: string[]): void {
bindSimulatorResource(__filename, this, host);
super.bind(host, ops);
}
/** @internal */
public _toInflight(): string {
return makeSimulatorJsClient(__filename, this);
}
Any hints for how to approach this issue? perhaps it is the wrong solution... @Chriscbr @eladb ?
@ainvoner wrote:
the simulator doesn't know this resource
Yes, it seems like simulator.json
currently only includes objects that implement toSimulator()
(see code.
I think we should include all nodes in simulator.json
and provide a default inflight client implementation that will be able to call visualModel
for example.
@ainvoner I'm curious why is the visual model defined inflight? I'm not sure if there's a need to perform asynchronous work or access runtime state in order to decide what components a resource has in its interaction panel.
I was imagining perhaps the UX could be more responsive for users if the visual model (text fields, links, buttons, other inputs) are all defined in preflight (at compile time), only allowing each component to reference inflight code if something needs to be dynamically fetched or run.
For example, let's say in the Website class, I want the interaction panel to show a link to the website's URL, and a graph of its metrics. As soon as I click on a Website in my app in the Wing Console, I want to see all of the available interactions, and the URL to show immediately - I'm okay if the graph loads later. If the Wing Console does not have information about interaction view until the inflight method completes, then then any one component could become a bottleneck:
pub inflight visualModel(): std.VisualModel {
let model = new std.VisualModelBuilder();
model.addLink("Website URL", this.url);
model.addImage("Page views", this.generateGraph()); // may take a few seconds to render
return model.buildVisualModel();
}
The visual model might include inflight information. Think number of objects in a bucket.
The actual UI components would still be fixed, no? In your example I might model it like
class Bucket {
init() {
// ...
this.visualModel.addField("Number of objects", inflight (): str {
return "${this.list().length}";
});
this.visualModel.addTable("Objects", this.tableEntries, pagination: true);
}
inflight tableEntries(pageIndex: num): std.DisplayTableRow[] {
// display keys and sizes of objects
}
}
this.tableEntries
This is not really supported at the moment as far as I know.
Also - this implies that the model itself is not serializable because we can't serialize functions (as of yet).
This is not really supported at the moment as far as I know.
You're right, for now you'd have to create an anonymous closure like:
inflight (pageIdx) => { return this.tableEntries(pageIdx); }
just so that the compiler could turn it into a class with a toInflight()
implementation. But I think we can address that separately.
Also - this implies that the model itself is not serializable because we can't serialize functions (as of yet).
Since the visual model would be configured in preflight, the code for it would be compiled and emitted as JavaScript, and runnable through the simulator. Check out this PR: https://github.com/winglang/wing/pull/4605
In the future, instead of modeling the information inside simulator.json
, we might refactor things over to tree.json
(a file we generate for all targets) so that the same resource interaction panel works when you're viewing your simulated app and your production app.
the code for it would be compiled and emitted as JavaScript,
This would require some compute container like a cloud.Function
...
At any rate - I think making this a simple inflight API is a reasonable first step.
@ainvoner and I chatted about this offline. We acknowledged the two approaches (defining the visual model preflight vs inflight) would have implications for how the interaction panel is rendered: if the collection of visual components is defined in preflight, each component can be re-rendered/updated separately by Wing Console, and if the list of components is defined inflight, the whole interaction panel needs to be re-rendered/updated together each time.
But Shai raised to my attention that calling inflight methods on user-defined resources is a capability that has been missing from the Wing Simulator for a long time now, so adding that would be useful for the console (both for implementing this Visual API MVP, and other console features).
Another requirement we discussed was the visual model cannot be a flat list of components, as the components should be composable (say you want to group multiple interactive things into a Layout, or a Tab, etc.).
Feature Spec
As part of my custom resource development in Wing, I'm also able to describe how it's interaction panel will look like in Wing Console.
(I prefer Option 2)
Option 1
very general purpose and flexible, but too difficult to use. I think we should be more opinionated
Usage in typescript (with LayoutMeta)
Option 2
Opinionated UI structure for each UI component type
Usage in typescript (with LayoutMeta)
Use Cases
Add ability to interact with new custom resources from the Wing Console
Implementation Notes
No response
Component
SDK, Wing Console
Community Notes