Bootstrap layout on first launch (#186)

This commit is contained in:
Evan Simkowitz 2024-07-31 21:27:46 -07:00 committed by GitHub
parent 8ebde7e766
commit 74e86ef0cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 249 additions and 32 deletions

View File

@ -149,6 +149,7 @@ tasks:
- "pkg/wstore/*.go"
- "pkg/wshrpc/**/*.go"
- "pkg/tsgen/**/*.go"
- "pkg/eventbus/eventbus.go"
generates:
- frontend/types/gotypes.d.ts
- pkg/wshrpc/wshclient/wshclient.go

View File

@ -11,6 +11,7 @@ import { getLayoutStateAtomForTab } from "frontend/layout/lib/layoutAtom";
import { layoutTreeStateReducer } from "frontend/layout/lib/layoutState";
import { handleIncomingRpcMessage } from "@/app/store/wshrpc";
import { LayoutTreeInsertNodeAtIndexAction } from "@/layout/lib/model";
import { getWSServerEndpoint, getWebServerEndpoint } from "@/util/endpoints";
import * as layoututil from "@/util/layoututil";
import { produce } from "immer";
@ -247,28 +248,49 @@ function handleWSEventMessage(msg: WSEventType) {
}
if (msg.eventtype == "layoutaction") {
const layoutAction: WSLayoutActionData = msg.data;
if (layoutAction.actiontype == LayoutTreeActionType.InsertNode) {
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
type: LayoutTreeActionType.InsertNode,
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, {
blockId: layoutAction.blockid,
}),
};
runLayoutAction(layoutAction.tabid, insertNodeAction);
} else if (layoutAction.actiontype == LayoutTreeActionType.DeleteNode) {
const layoutStateAtom = getLayoutStateAtomForTab(
layoutAction.tabid,
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", layoutAction.tabid))
);
const curState = globalStore.get(layoutStateAtom);
const leafId = layoututil.findLeafIdFromBlockId(curState, layoutAction.blockid);
const deleteNodeAction = {
type: LayoutTreeActionType.DeleteNode,
nodeId: leafId,
};
runLayoutAction(layoutAction.tabid, deleteNodeAction);
} else {
console.log("unsupported layout action", layoutAction);
switch (layoutAction.actiontype) {
case LayoutTreeActionType.InsertNode: {
const insertNodeAction: LayoutTreeInsertNodeAction<TabLayoutData> = {
type: LayoutTreeActionType.InsertNode,
node: newLayoutNode<TabLayoutData>(undefined, undefined, undefined, {
blockId: layoutAction.blockid,
}),
};
runLayoutAction(layoutAction.tabid, insertNodeAction);
break;
}
case LayoutTreeActionType.DeleteNode: {
const layoutStateAtom = getLayoutStateAtomForTab(
layoutAction.tabid,
WOS.getWaveObjectAtom<Tab>(WOS.makeORef("tab", layoutAction.tabid))
);
const curState = globalStore.get(layoutStateAtom);
const leafId = layoututil.findLeafIdFromBlockId(curState, layoutAction.blockid);
const deleteNodeAction = {
type: LayoutTreeActionType.DeleteNode,
nodeId: leafId,
};
runLayoutAction(layoutAction.tabid, deleteNodeAction);
break;
}
case LayoutTreeActionType.InsertNodeAtIndex: {
if (!layoutAction.indexarr) {
console.error("Cannot apply eventbus layout action InsertNodeAtIndex, indexarr field is missing.");
break;
}
const insertAction: LayoutTreeInsertNodeAtIndexAction<TabLayoutData> = {
type: LayoutTreeActionType.InsertNodeAtIndex,
node: newLayoutNode<TabLayoutData>(undefined, layoutAction.nodesize, undefined, {
blockId: layoutAction.blockid,
}),
indexArr: layoutAction.indexarr,
};
runLayoutAction(layoutAction.tabid, insertAction);
break;
}
default:
console.log("unsupported layout action", layoutAction);
break;
}
return;
}

View File

@ -26,6 +26,9 @@ class ClientServiceType {
AgreeTos(): Promise<void> {
return WOS.callBackendService("client", "AgreeTos", Array.from(arguments))
}
BootstrapStarterLayout(): Promise<void> {
return WOS.callBackendService("client", "BootstrapStarterLayout", Array.from(arguments))
}
FocusWindow(arg2: string): Promise<void> {
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
}
@ -89,6 +92,9 @@ class ObjectServiceType {
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments))
}
CreateBlock_NoUI(arg2: string, arg3: BlockDef, arg4: RuntimeOpts): Promise<Block> {
return WOS.callBackendService("object", "CreateBlock_NoUI", Array.from(arguments))
}
// @returns object updates
DeleteBlock(blockId: string): Promise<void> {

View File

@ -226,6 +226,34 @@ export function findNextInsertLocation<T>(
return { node: insertLoc?.node, index: insertLoc?.index };
}
/**
* Traverse the layout tree using the supplied index array to find the node to insert at.
* @param node The node to start the search from.
* @param indexArr The array of indices to aid in the traversal.
* @returns The node to insert into and the index at which to insert.
*/
export function findInsertLocationFromIndexArr<T>(
node: LayoutNode<T>,
indexArr: number[]
): { node: LayoutNode<T>; index: number } {
function normalizeIndex(index: number) {
const childrenLength = node.children?.length ?? 1;
const lastChildIndex = childrenLength - 1;
if (index < 0) {
return childrenLength - Math.max(index, -childrenLength);
}
return Math.min(index, lastChildIndex);
}
if (indexArr.length == 0) {
return;
}
const nextIndex = normalizeIndex(indexArr.shift());
if (indexArr.length == 0 || !node.children) {
return { node, index: nextIndex };
}
return findInsertLocationFromIndexArr<T>(node.children[nextIndex], indexArr);
}
function findNextInsertLocationHelper<T>(
node: LayoutNode<T>,
maxChildren: number,

View File

@ -6,6 +6,7 @@ import {
addChildAt,
addIntermediateNode,
balanceNode,
findInsertLocationFromIndexArr,
findNextInsertLocation,
findNode,
findParent,
@ -19,6 +20,7 @@ import {
LayoutTreeComputeMoveNodeAction,
LayoutTreeDeleteNodeAction,
LayoutTreeInsertNodeAction,
LayoutTreeInsertNodeAtIndexAction,
LayoutTreeMagnifyNodeToggleAction,
LayoutTreeMoveNodeAction,
LayoutTreeResizeNodeAction,
@ -97,6 +99,10 @@ function layoutTreeStateReducerInner<T>(layoutTreeState: LayoutTreeState<T>, act
insertNode(layoutTreeState, action as LayoutTreeInsertNodeAction<T>);
layoutTreeState.generation++;
break;
case LayoutTreeActionType.InsertNodeAtIndex:
insertNodeAtIndex(layoutTreeState, action as LayoutTreeInsertNodeAtIndexAction<T>);
layoutTreeState.generation++;
break;
case LayoutTreeActionType.DeleteNode:
deleteNode(layoutTreeState, action as LayoutTreeDeleteNodeAction);
layoutTreeState.generation++;
@ -370,7 +376,7 @@ function moveNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeMove
function insertNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeInsertNodeAction<T>) {
if (!action?.node) {
console.error("no insert node action provided");
console.error("insertNode cannot run, no insert node action provided");
return;
}
if (!layoutTreeState.rootNode) {
@ -386,6 +392,28 @@ function insertNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeIn
layoutTreeState.leafs = leafs;
}
function insertNodeAtIndex<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeInsertNodeAtIndexAction<T>) {
if (!action?.node || !action?.indexArr) {
console.error("insertNodeAtIndex cannot run, either node or indexArr field is missing");
return;
}
if (!layoutTreeState.rootNode) {
const { node: balancedNode, leafs } = balanceNode(action.node);
layoutTreeState.rootNode = balancedNode;
layoutTreeState.leafs = leafs;
return;
}
const insertLoc = findInsertLocationFromIndexArr(layoutTreeState.rootNode, action.indexArr);
if (!insertLoc) {
console.error("insertNodeAtIndex unable to find insert location");
return;
}
addChildAt(insertLoc.node, insertLoc.index + 1, action.node);
const { node: newRootNode, leafs } = balanceNode(layoutTreeState.rootNode);
layoutTreeState.rootNode = newRootNode;
layoutTreeState.leafs = leafs;
}
function swapNode<T>(layoutTreeState: LayoutTreeState<T>, action: LayoutTreeSwapNodeAction) {
console.log("swapNode", layoutTreeState, action);

View File

@ -41,6 +41,7 @@ export enum LayoutTreeActionType {
ClearPendingAction = "clearpending",
ResizeNode = "resize",
InsertNode = "insert",
InsertNodeAtIndex = "insertatindex",
DeleteNode = "delete",
MagnifyNodeToggle = "magnify",
}
@ -104,6 +105,22 @@ export interface LayoutTreeInsertNodeAction<T> extends LayoutTreeAction {
node: LayoutNode<T>;
}
/**
* Action for inserting a node into the layout tree at the specified index.
*/
export interface LayoutTreeInsertNodeAtIndexAction<T> extends LayoutTreeAction {
type: LayoutTreeActionType.InsertNodeAtIndex;
/**
* The node to insert.
*/
node: LayoutNode<T>;
/**
* The array of indices to traverse when inserting the node.
* The last index is the index within the parent node where the node should be inserted.
*/
indexArr: number[];
}
/**
* Action for deleting a node from the layout tree.
*/

View File

@ -472,6 +472,8 @@ declare global {
tabid: string;
actiontype: string;
blockid: string;
nodesize?: number;
indexarr?: number[];
};
// webcmd.WSRpcCommand

View File

@ -60,6 +60,8 @@ type WSLayoutActionData struct {
TabId string `json:"tabid"`
ActionType string `json:"actiontype"`
BlockId string `json:"blockid"`
NodeSize uint `json:"nodesize,omitempty"`
IndexArr []int `json:"indexarr,omitempty"`
}
var globalLock = &sync.Mutex{}

View File

@ -6,8 +6,11 @@ package clientservice
import (
"context"
"fmt"
"log"
"time"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/service/objectservice"
"github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/wstore"
)
@ -86,5 +89,102 @@ func (cs *ClientService) AgreeTos(ctx context.Context) (wstore.UpdatesRtnType, e
if err != nil {
return nil, fmt.Errorf("error updating client data: %w", err)
}
cs.BootstrapStarterLayout(ctx)
return wstore.ContextGetUpdatesRtn(ctx), nil
}
type PortableLayout []struct {
IndexArr []int
Size uint
BlockDef *wstore.BlockDef
}
func (cs *ClientService) BootstrapStarterLayout(ctx context.Context) error {
ctx, cancelFn := context.WithTimeout(ctx, 2*time.Second)
defer cancelFn()
client, err := wstore.DBGetSingleton[*wstore.Client](ctx)
if err != nil {
log.Printf("unable to find client: %v\n", err)
return fmt.Errorf("unable to find client: %w", err)
}
if len(client.WindowIds) < 1 {
return fmt.Errorf("error bootstrapping layout, no windows exist")
}
windowId := client.WindowIds[0]
window, err := wstore.DBMustGet[*wstore.Window](ctx, windowId)
if err != nil {
return fmt.Errorf("error getting window: %w", err)
}
tabId := window.ActiveTabId
starterLayout := PortableLayout{
{IndexArr: []int{0}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "term",
wstore.MetaKey_Controller: "shell",
},
}},
{IndexArr: []int{1}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "cpuplot",
},
}},
{IndexArr: []int{1, 1}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "web",
wstore.MetaKey_Url: "https://github.com/wavetermdev/waveterm",
},
}},
{IndexArr: []int{1, 2}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "preview",
wstore.MetaKey_File: "~",
},
}},
{IndexArr: []int{2}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "term",
wstore.MetaKey_Controller: "shell",
},
}},
{IndexArr: []int{2, 1}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "waveai",
},
}},
{IndexArr: []int{2, 2}, BlockDef: &wstore.BlockDef{
Meta: wstore.MetaMapType{
wstore.MetaKey_View: "web",
wstore.MetaKey_Url: "https://www.youtube.com/embed/cKqsw_sAsU8",
},
}},
}
objsvc := &objectservice.ObjectService{}
for i := 0; i < len(starterLayout); i++ {
layoutAction := starterLayout[i]
blockData, err := objsvc.CreateBlock_NoUI(ctx, tabId, layoutAction.BlockDef, &wstore.RuntimeOpts{})
if err != nil {
return fmt.Errorf("unable to create block for starter layout: %w", err)
}
eventbus.SendEventToWindow(windowId, eventbus.WSEventType{
EventType: eventbus.WSEvent_LayoutAction,
Data: &eventbus.WSLayoutActionData{
ActionType: "insertatindex",
TabId: tabId,
BlockId: blockData.OID,
IndexArr: layoutAction.IndexArr,
NodeSize: layoutAction.Size,
},
})
}
return nil
}

View File

@ -178,6 +178,22 @@ func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta {
}
}
func (svc *ObjectService) CreateBlock_NoUI(ctx context.Context, tabId string, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (*wstore.Block, error) {
blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts)
if err != nil {
return nil, fmt.Errorf("error creating block: %w", err)
}
controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "")
if controllerName != "" {
err = blockcontroller.StartBlockController(ctx, tabId, blockData.OID)
if err != nil {
return nil, fmt.Errorf("error starting block controller: %w", err)
}
}
return blockData, nil
}
func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wstore.BlockDef, rtOpts *wstore.RuntimeOpts) (string, wstore.UpdatesRtnType, error) {
if uiContext.ActiveTabId == "" {
return "", nil, fmt.Errorf("no active tab")
@ -185,17 +201,12 @@ func (svc *ObjectService) CreateBlock(uiContext wstore.UIContext, blockDef *wsto
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
ctx = wstore.ContextWithUpdates(ctx)
blockData, err := wstore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts)
blockData, err := svc.CreateBlock_NoUI(ctx, uiContext.ActiveTabId, blockDef, rtOpts)
if err != nil {
return "", nil, fmt.Errorf("error creating block: %w", err)
}
controllerName := blockData.Meta.GetString(wstore.MetaKey_Controller, "")
if controllerName != "" {
err = blockcontroller.StartBlockController(ctx, uiContext.ActiveTabId, blockData.OID)
if err != nil {
return "", nil, fmt.Errorf("error starting block controller: %w", err)
}
return "", nil, err
}
return blockData.OID, wstore.ContextGetUpdatesRtn(ctx), nil
}