From c9c555452ae12a7365caead5ae5807d9fa8addaf Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Tue, 27 Aug 2024 18:38:57 -0700 Subject: [PATCH] Establish wlayout for coordinating backend layout actions (#282) --- frontend/app/store/global.ts | 52 ------ frontend/app/store/services.ts | 6 - frontend/layout/lib/layoutAtom.ts | 5 + frontend/layout/lib/layoutModel.ts | 69 +++++++- frontend/layout/lib/layoutModelHooks.ts | 3 +- frontend/layout/lib/types.ts | 5 + frontend/types/gotypes.d.ts | 3 +- pkg/eventbus/eventbus.go | 5 - pkg/service/clientservice/clientservice.go | 101 +----------- pkg/service/objectservice/objectservice.go | 24 +-- pkg/service/windowservice/windowservice.go | 25 +-- pkg/waveobj/wtype.go | 21 +-- pkg/wcore/wcore.go | 10 +- pkg/wlayout/wlayout.go | 176 +++++++++++++++++++++ pkg/wshrpc/wshserver/wshserver.go | 26 ++- 15 files changed, 294 insertions(+), 237 deletions(-) create mode 100644 pkg/wlayout/wlayout.go diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index cfab6ce9..5a0faa57 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -7,13 +7,11 @@ import { getLayoutModelForTabById, LayoutTreeActionType, LayoutTreeInsertNodeAction, - LayoutTreeInsertNodeAtIndexAction, newLayoutNode, } from "@/layout/index"; import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; import * as util from "@/util/util"; -import { fireAndForget } from "@/util/util"; import * as jotai from "jotai"; import * as rxjs from "rxjs"; import { modalsModel } from "./modalmodel"; @@ -360,56 +358,6 @@ function handleWSEventMessage(msg: WSEventType) { handleIncomingRpcMessage(rpcMsg, handleWaveEvent); return; } - if (msg.eventtype == "layoutaction") { - const layoutAction: LayoutActionData = msg.data; - const tabId = layoutAction.tabid; - const layoutModel = getLayoutModelForTabById(tabId); - switch (layoutAction.actiontype) { - case LayoutTreeActionType.InsertNode: { - const insertNodeAction: LayoutTreeInsertNodeAction = { - type: LayoutTreeActionType.InsertNode, - node: newLayoutNode(undefined, undefined, undefined, { - blockId: layoutAction.blockid, - }), - magnified: layoutAction.magnified, - }; - layoutModel.treeReducer(insertNodeAction); - break; - } - case LayoutTreeActionType.DeleteNode: { - const leaf = layoutModel?.getNodeByBlockId(layoutAction.blockid); - if (leaf) { - fireAndForget(() => layoutModel.closeNode(leaf.id)); - } else { - console.error( - "Cannot apply eventbus layout action DeleteNode, could not find leaf node with blockId", - layoutAction.blockid - ); - } - break; - } - case LayoutTreeActionType.InsertNodeAtIndex: { - if (!layoutAction.indexarr) { - console.error("Cannot apply eventbus layout action InsertNodeAtIndex, indexarr field is missing."); - break; - } - const insertAction: LayoutTreeInsertNodeAtIndexAction = { - type: LayoutTreeActionType.InsertNodeAtIndex, - node: newLayoutNode(undefined, layoutAction.nodesize, undefined, { - blockId: layoutAction.blockid, - }), - indexArr: layoutAction.indexarr, - magnified: layoutAction.magnified, - }; - layoutModel.treeReducer(insertAction); - break; - } - default: - console.warn("unsupported layout action", layoutAction); - break; - } - return; - } // we send to two subjects just eventType and eventType|oref // we don't use getORefSubject here because we don't want to create a new subject const eventSubject = eventSubjects.get(msg.eventtype); diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 8e58090c..060d1bd7 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -26,9 +26,6 @@ class ClientServiceType { AgreeTos(): Promise { return WOS.callBackendService("client", "AgreeTos", Array.from(arguments)) } - BootstrapStarterLayout(): Promise { - return WOS.callBackendService("client", "BootstrapStarterLayout", Array.from(arguments)) - } FocusWindow(arg2: string): Promise { return WOS.callBackendService("client", "FocusWindow", Array.from(arguments)) } @@ -103,9 +100,6 @@ class ObjectServiceType { CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise { return WOS.callBackendService("object", "CreateBlock", Array.from(arguments)) } - CreateBlock_NoUI(arg2: string, arg3: BlockDef, arg4: RuntimeOpts): Promise { - return WOS.callBackendService("object", "CreateBlock_NoUI", Array.from(arguments)) - } // @returns object updates DeleteBlock(blockId: string): Promise { diff --git a/frontend/layout/lib/layoutAtom.ts b/frontend/layout/lib/layoutAtom.ts index a4bd463b..53d0526f 100644 --- a/frontend/layout/lib/layoutAtom.ts +++ b/frontend/layout/lib/layoutAtom.ts @@ -29,6 +29,7 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom): WritableLayo rootNode: layoutStateData?.rootnode, focusedNodeId: layoutStateData?.focusednodeid, magnifiedNodeId: layoutStateData?.magnifiednodeid, + pendingBackendActions: layoutStateData?.pendingbackendactions, generation: get(generationAtom), }; return layoutTreeState; @@ -41,6 +42,10 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom): WritableLayo waveObjVal.rootnode = value.rootNode; waveObjVal.magnifiednodeid = value.magnifiedNodeId; waveObjVal.focusednodeid = value.focusedNodeId; + waveObjVal.leaforder = value.leafOrder; // only set leaforder, never get it, since this value is driven by the frontend + waveObjVal.pendingbackendactions = value.pendingBackendActions?.length + ? value.pendingBackendActions + : undefined; set(generationAtom, value.generation); set(stateAtom, waveObjVal); } diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 6edb233a..e84f009a 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -6,7 +6,7 @@ import { Atom, atom, Getter, PrimitiveAtom, Setter } from "jotai"; import { splitAtom } from "jotai/utils"; import { createRef, CSSProperties } from "react"; import { debounce } from "throttle-debounce"; -import { balanceNode, findNode, walkNodes } from "./layoutNode"; +import { balanceNode, findNode, newLayoutNode, walkNodes } from "./layoutNode"; import { computeMoveNode, deleteNode, @@ -369,17 +369,72 @@ export class LayoutModel { * Callback that is invoked when the tree state has been updated on the backend. This ensures the model is updated if the atom is not fully loaded when the model is first instantiated. * @param force Whether to force the tree state to update, regardless of whether the state is already up to date. */ - updateTreeState(force = false) { + async updateTreeState(force = false) { const treeState = this.getter(this.treeStateAtom); // Only update the local tree state if it is different from the one in the backend. This function is called even when the update was initiated by the LayoutModel, so we need to filter out false positives or we'll enter an infinite loop. if ( force || !this.treeState?.rootNode || !this.treeState?.generation || - treeState?.generation > this.treeState.generation + treeState?.generation > this.treeState.generation || + treeState?.pendingBackendActions?.length ) { this.treeState = treeState; - this.updateTree(); + + if (this.treeState.pendingBackendActions?.length) { + const actions = this.treeState.pendingBackendActions; + this.treeState.pendingBackendActions = undefined; + for (const action of actions) { + switch (action.actiontype) { + case LayoutTreeActionType.InsertNode: { + const insertNodeAction: LayoutTreeInsertNodeAction = { + type: LayoutTreeActionType.InsertNode, + node: newLayoutNode(undefined, undefined, undefined, { + blockId: action.blockid, + }), + magnified: action.magnified, + }; + this.treeReducer(insertNodeAction); + break; + } + case LayoutTreeActionType.DeleteNode: { + const leaf = this?.getNodeByBlockId(action.blockid); + if (leaf) { + await this.closeNode(leaf.id); + } else { + console.error( + "Cannot apply eventbus layout action DeleteNode, could not find leaf node with blockId", + action.blockid + ); + } + break; + } + case LayoutTreeActionType.InsertNodeAtIndex: { + if (!action.indexarr) { + console.error( + "Cannot apply eventbus layout action InsertNodeAtIndex, indexarr field is missing." + ); + break; + } + const insertAction: LayoutTreeInsertNodeAtIndexAction = { + type: LayoutTreeActionType.InsertNodeAtIndex, + node: newLayoutNode(undefined, action.nodesize, undefined, { + blockId: action.blockid, + }), + indexArr: action.indexarr, + magnified: action.magnified, + }; + this.treeReducer(insertAction); + break; + } + default: + console.warn("unsupported layout action", action); + break; + } + } + } else { + this.updateTree(); + } } } @@ -407,9 +462,9 @@ export class LayoutModel { this.leafs, newLeafs.sort((a, b) => a.id.localeCompare(b.id)) ); - const newLeafOrder = getLeafOrder(newLeafs, newAdditionalProps); - this.setter(this.leafOrder, newLeafOrder); - this.validateFocusedNode(newLeafOrder); + this.treeState.leafOrder = getLeafOrder(newLeafs, newAdditionalProps); + this.setter(this.leafOrder, this.treeState.leafOrder); + this.validateFocusedNode(this.treeState.leafOrder); this.cleanupNodeModels(); } }; diff --git a/frontend/layout/lib/layoutModelHooks.ts b/frontend/layout/lib/layoutModelHooks.ts index 27927de1..630e8ee0 100644 --- a/frontend/layout/lib/layoutModelHooks.ts +++ b/frontend/layout/lib/layoutModelHooks.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { atoms, globalStore, WOS } from "@/app/store/global"; +import { fireAndForget } from "@/util/util"; import useResizeObserver from "@react-hook/resize-observer"; import { Atom, useAtomValue } from "jotai"; import { CSSProperties, useEffect, useState } from "react"; @@ -23,7 +24,7 @@ export function getLayoutModelForTab(tabAtom: Atom): LayoutModel { } const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom); const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set); - globalStore.sub(layoutTreeStateAtom, () => layoutModel.updateTreeState()); + globalStore.sub(layoutTreeStateAtom, () => fireAndForget(() => layoutModel.updateTreeState())); layoutModelMap.set(tabId, layoutModel); return layoutModel; } diff --git a/frontend/layout/lib/types.ts b/frontend/layout/lib/types.ts index 64307f5c..83c58bf7 100644 --- a/frontend/layout/lib/types.ts +++ b/frontend/layout/lib/types.ts @@ -249,6 +249,11 @@ export type LayoutTreeState = { rootNode: LayoutNode; focusedNodeId?: string; magnifiedNodeId?: string; + /** + * A computed ordered list of leafs in the layout. This value is driven by the LayoutModel and should not be read when updated from the backend. + */ + leafOrder?: string[]; + pendingBackendActions: LayoutActionData[]; generation: number; }; diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c2c7680f..351a675e 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -209,7 +209,6 @@ declare global { // waveobj.LayoutActionData type LayoutActionData = { - tabid: string; actiontype: string; blockid: string; nodesize?: number; @@ -222,6 +221,8 @@ declare global { rootnode?: any; magnifiednodeid?: string; focusednodeid?: string; + leaforder?: string[]; + pendingbackendactions?: LayoutActionData[]; }; // waveobj.MetaTSType diff --git a/pkg/eventbus/eventbus.go b/pkg/eventbus/eventbus.go index 2ea1ed92..8661f7f9 100644 --- a/pkg/eventbus/eventbus.go +++ b/pkg/eventbus/eventbus.go @@ -51,11 +51,6 @@ type WindowWatchData struct { WatchedORefs map[waveobj.ORef]bool } -const ( - WSLayoutActionType_Insert = "insert" - WSLayoutActionType_Remove = "delete" -) - var globalLock = &sync.Mutex{} var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData diff --git a/pkg/service/clientservice/clientservice.go b/pkg/service/clientservice/clientservice.go index 8dfdda62..af0633d3 100644 --- a/pkg/service/clientservice/clientservice.go +++ b/pkg/service/clientservice/clientservice.go @@ -6,15 +6,13 @@ package clientservice import ( "context" "fmt" - "log" "time" - "github.com/wavetermdev/thenextwave/pkg/eventbus" "github.com/wavetermdev/thenextwave/pkg/remote/conncontroller" - "github.com/wavetermdev/thenextwave/pkg/service/objectservice" "github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wcore" + "github.com/wavetermdev/thenextwave/pkg/wlayout" "github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -97,101 +95,6 @@ func (cs *ClientService) AgreeTos(ctx context.Context) (waveobj.UpdatesRtnType, if err != nil { return nil, fmt.Errorf("error updating client data: %w", err) } - cs.BootstrapStarterLayout(ctx) + wlayout.BootstrapStarterLayout(ctx) return waveobj.ContextGetUpdatesRtn(ctx), nil } - -type PortableLayout []struct { - IndexArr []int - Size uint - BlockDef *waveobj.BlockDef -} - -func (cs *ClientService) BootstrapStarterLayout(ctx context.Context) error { - ctx, cancelFn := context.WithTimeout(ctx, 2*time.Second) - defer cancelFn() - client, err := wstore.DBGetSingleton[*waveobj.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[*waveobj.Window](ctx, windowId) - if err != nil { - return fmt.Errorf("error getting window: %w", err) - } - - tabId := window.ActiveTabId - - starterLayout := PortableLayout{ - {IndexArr: []int{0}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "term", - waveobj.MetaKey_Controller: "shell", - }, - }}, - {IndexArr: []int{1}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "cpuplot", - }, - }}, - {IndexArr: []int{1, 1}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "web", - waveobj.MetaKey_Url: "https://github.com/wavetermdev/waveterm", - }, - }}, - {IndexArr: []int{1, 2}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "preview", - waveobj.MetaKey_File: "~", - }, - }}, - {IndexArr: []int{2}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "help", - }, - }}, - {IndexArr: []int{2, 1}, BlockDef: &waveobj.BlockDef{ - Meta: waveobj.MetaMapType{ - waveobj.MetaKey_View: "waveai", - }, - }}, - // {IndexArr: []int{2, 2}, BlockDef: &wstore.BlockDef{ - // Meta: wstore.MetaMapType{ - // waveobj.MetaKey_View: "web", - // waveobj.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, &waveobj.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: &waveobj.LayoutActionData{ - ActionType: "insertatindex", - TabId: tabId, - BlockId: blockData.OID, - IndexArr: layoutAction.IndexArr, - NodeSize: layoutAction.Size, - }, - }) - } - return nil -} diff --git a/pkg/service/objectservice/objectservice.go b/pkg/service/objectservice/objectservice.go index 9710ceb3..3188cad9 100644 --- a/pkg/service/objectservice/objectservice.go +++ b/pkg/service/objectservice/objectservice.go @@ -14,6 +14,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wcore" + "github.com/wavetermdev/thenextwave/pkg/wlayout" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -87,6 +88,11 @@ func (svc *ObjectService) AddTabToWorkspace(uiContext waveobj.UIContext, tabName if err != nil { return "", nil, fmt.Errorf("error creating tab: %w", err) } + + err = wlayout.ApplyPortableLayout(ctx, tabId, wlayout.GetNewTabLayout()) + if err != nil { + return "", nil, fmt.Errorf("error applying new tab layout: %w", err) + } return tabId, waveobj.ContextGetUpdatesRtn(ctx), nil } @@ -169,22 +175,6 @@ func (svc *ObjectService) CreateBlock_Meta() tsgenmeta.MethodMeta { } } -func (svc *ObjectService) CreateBlock_NoUI(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.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(waveobj.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 waveobj.UIContext, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (string, waveobj.UpdatesRtnType, error) { if uiContext.ActiveTabId == "" { return "", nil, fmt.Errorf("no active tab") @@ -193,7 +183,7 @@ func (svc *ObjectService) CreateBlock(uiContext waveobj.UIContext, blockDef *wav defer cancelFn() ctx = waveobj.ContextWithUpdates(ctx) - blockData, err := svc.CreateBlock_NoUI(ctx, uiContext.ActiveTabId, blockDef, rtOpts) + blockData, err := wcore.CreateBlock(ctx, uiContext.ActiveTabId, blockDef, rtOpts) if err != nil { return "", nil, err } diff --git a/pkg/service/windowservice/windowservice.go b/pkg/service/windowservice/windowservice.go index 1b422e43..42976839 100644 --- a/pkg/service/windowservice/windowservice.go +++ b/pkg/service/windowservice/windowservice.go @@ -15,6 +15,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wcore" + "github.com/wavetermdev/thenextwave/pkg/wlayout" "github.com/wavetermdev/thenextwave/pkg/wstore" ) @@ -100,10 +101,6 @@ func (svc *WindowService) MoveBlockToNewWindow_Meta() tsgenmeta.MethodMeta { func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId string, blockId string) (waveobj.UpdatesRtnType, error) { log.Printf("MoveBlockToNewWindow(%s, %s)", currentTabId, blockId) ctx = waveobj.ContextWithUpdates(ctx) - curWindowId, err := wstore.DBFindWindowForTabId(ctx, currentTabId) - if err != nil { - return nil, fmt.Errorf("error finding window for current-tab: %w", err) - } tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, currentTabId) if err != nil { return nil, fmt.Errorf("error getting tab: %w", err) @@ -135,21 +132,13 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId if !windowCreated { return nil, fmt.Errorf("new window not created") } - eventbus.SendEventToWindow(curWindowId, eventbus.WSEventType{ - EventType: eventbus.WSEvent_LayoutAction, - Data: waveobj.LayoutActionData{ - ActionType: eventbus.WSLayoutActionType_Remove, - TabId: currentTabId, - BlockId: blockId, - }, + wlayout.QueueLayoutActionForTab(ctx, currentTabId, waveobj.LayoutActionData{ + ActionType: wlayout.LayoutActionDataType_Remove, + BlockId: blockId, }) - eventbus.SendEventToWindow(newWindow.OID, eventbus.WSEventType{ - EventType: eventbus.WSEvent_LayoutAction, - Data: waveobj.LayoutActionData{ - ActionType: eventbus.WSLayoutActionType_Insert, - TabId: newWindow.ActiveTabId, - BlockId: blockId, - }, + wlayout.QueueLayoutActionForTab(ctx, newWindow.ActiveTabId, waveobj.LayoutActionData{ + ActionType: wlayout.LayoutActionDataType_Insert, + BlockId: blockId, }) return waveobj.ContextGetUpdatesRtn(ctx), nil } diff --git a/pkg/waveobj/wtype.go b/pkg/waveobj/wtype.go index 522d1e75..f18466e9 100644 --- a/pkg/waveobj/wtype.go +++ b/pkg/waveobj/wtype.go @@ -174,21 +174,22 @@ func (t *Tab) GetBlockORefs() []ORef { } type LayoutActionData struct { - TabId string `json:"tabid"` ActionType string `json:"actiontype"` BlockId string `json:"blockid"` - NodeSize uint `json:"nodesize,omitempty"` - IndexArr []int `json:"indexarr,omitempty"` - Magnified bool `json:"magnified,omitempty"` + NodeSize *uint `json:"nodesize,omitempty"` + IndexArr *[]int `json:"indexarr,omitempty"` + Magnified *bool `json:"magnified,omitempty"` } type LayoutState struct { - OID string `json:"oid"` - Version int `json:"version"` - RootNode any `json:"rootnode,omitempty"` - MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` - FocusedNodeId string `json:"focusednodeid,omitempty"` - Meta MetaMapType `json:"meta,omitempty"` + OID string `json:"oid"` + Version int `json:"version"` + RootNode any `json:"rootnode,omitempty"` + MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` + FocusedNodeId string `json:"focusednodeid,omitempty"` + LeafOrder *[]string `json:"leaforder,omitempty"` + PendingBackendActions *[]LayoutActionData `json:"pendingbackendactions,omitempty"` + Meta MetaMapType `json:"meta,omitempty"` } func (*LayoutState) GetOType() string { diff --git a/pkg/wcore/wcore.go b/pkg/wcore/wcore.go index fdfeced7..adc54296 100644 --- a/pkg/wcore/wcore.go +++ b/pkg/wcore/wcore.go @@ -168,18 +168,18 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) { return client, nil } -func CreateBlock(ctx context.Context, createBlockCmd wshrpc.CommandCreateBlockData) (*waveobj.ORef, error) { - tabId := createBlockCmd.TabId - blockData, err := wstore.CreateBlock(ctx, tabId, createBlockCmd.BlockDef, createBlockCmd.RtOpts) +func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.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(waveobj.MetaKey_Controller, "") if controllerName != "" { - err = blockcontroller.StartBlockController(ctx, createBlockCmd.TabId, blockData.OID) + err = blockcontroller.StartBlockController(ctx, tabId, blockData.OID) if err != nil { return nil, fmt.Errorf("error starting block controller: %w", err) } } - return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}, nil + + return blockData, nil } diff --git a/pkg/wlayout/wlayout.go b/pkg/wlayout/wlayout.go new file mode 100644 index 00000000..f2ab889c --- /dev/null +++ b/pkg/wlayout/wlayout.go @@ -0,0 +1,176 @@ +// Copyright 2024, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package wlayout + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/wavetermdev/thenextwave/pkg/waveobj" + "github.com/wavetermdev/thenextwave/pkg/wcore" + "github.com/wavetermdev/thenextwave/pkg/wstore" +) + +const ( + LayoutActionDataType_Insert = "insert" + LayoutActionDataType_InsertAtIndex = "insertatindex" + LayoutActionDataType_Remove = "delete" +) + +type PortableLayout []struct { + IndexArr []int `json:"indexarr"` + Size *uint `json:"size,omitempty"` + BlockDef *waveobj.BlockDef `json:"blockdef"` +} + +func GetStarterLayout() PortableLayout { + return PortableLayout{ + {IndexArr: []int{0}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "term", + waveobj.MetaKey_Controller: "shell", + }, + }}, + {IndexArr: []int{1}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "cpuplot", + }, + }}, + {IndexArr: []int{1, 1}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "web", + waveobj.MetaKey_Url: "https://github.com/wavetermdev/waveterm", + }, + }}, + {IndexArr: []int{1, 2}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "preview", + waveobj.MetaKey_File: "~", + }, + }}, + {IndexArr: []int{2}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "help", + }, + }}, + {IndexArr: []int{2, 1}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "waveai", + }, + }}, + // {IndexArr: []int{2, 2}, BlockDef: &wstore.BlockDef{ + // Meta: wstore.MetaMapType{ + // waveobj.MetaKey_View: "web", + // waveobj.MetaKey_Url: "https://www.youtube.com/embed/cKqsw_sAsU8", + // }, + // }}, + } +} + +func GetNewTabLayout() PortableLayout { + return PortableLayout{ + {IndexArr: []int{0}, BlockDef: &waveobj.BlockDef{ + Meta: waveobj.MetaMapType{ + waveobj.MetaKey_View: "term", + waveobj.MetaKey_Controller: "shell", + }, + }}, + } +} + +func GetLayoutIdForTab(ctx context.Context, tabId string) (string, error) { + tabObj, err := wstore.DBGet[*waveobj.Tab](ctx, tabId) + if err != nil { + return "", fmt.Errorf("unable to get layout id for given tab id %s: %w", tabId, err) + } + return tabObj.LayoutState, nil +} + +func QueueLayoutAction(ctx context.Context, layoutStateId string, actions ...waveobj.LayoutActionData) error { + layoutStateObj, err := wstore.DBGet[*waveobj.LayoutState](ctx, layoutStateId) + if err != nil { + return fmt.Errorf("unable to get layout state for given id %s: %w", layoutStateId, err) + } + + if layoutStateObj.PendingBackendActions == nil { + layoutStateObj.PendingBackendActions = &actions + } else { + *layoutStateObj.PendingBackendActions = append(*layoutStateObj.PendingBackendActions, actions...) + } + + err = wstore.DBUpdate(ctx, layoutStateObj) + if err != nil { + return fmt.Errorf("unable to update layout state with new actions: %w", err) + } + return nil +} + +func QueueLayoutActionForTab(ctx context.Context, tabId string, actions ...waveobj.LayoutActionData) error { + layoutStateId, err := GetLayoutIdForTab(ctx, tabId) + if err != nil { + return err + } + + return QueueLayoutAction(ctx, layoutStateId, actions...) +} + +func ApplyPortableLayout(ctx context.Context, tabId string, layout PortableLayout) error { + actions := make([]waveobj.LayoutActionData, len(layout)) + for i := 0; i < len(layout); i++ { + layoutAction := layout[i] + + blockData, err := wcore.CreateBlock(ctx, tabId, layoutAction.BlockDef, &waveobj.RuntimeOpts{}) + if err != nil { + return fmt.Errorf("unable to create block to apply portable layout to tab %s: %w", tabId, err) + } + + actions[i] = waveobj.LayoutActionData{ + ActionType: LayoutActionDataType_InsertAtIndex, + BlockId: blockData.OID, + IndexArr: &layoutAction.IndexArr, + NodeSize: layoutAction.Size, + } + } + + err := QueueLayoutActionForTab(ctx, tabId, actions...) + if err != nil { + return fmt.Errorf("unable to queue layout actions for portable layout: %w", err) + } + + return nil +} + +func BootstrapStarterLayout(ctx context.Context) error { + ctx, cancelFn := context.WithTimeout(ctx, 2*time.Second) + defer cancelFn() + client, err := wstore.DBGetSingleton[*waveobj.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[*waveobj.Window](ctx, windowId) + if err != nil { + return fmt.Errorf("error getting window: %w", err) + } + + tabId := window.ActiveTabId + + starterLayout := GetStarterLayout() + + err = ApplyPortableLayout(ctx, tabId, starterLayout) + if err != nil { + return fmt.Errorf("error applying portable layout: %w", err) + } + + return nil +} diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 951ad2fd..bf55b8db 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -22,6 +22,7 @@ import ( "github.com/wavetermdev/thenextwave/pkg/waveai" "github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/wcore" + "github.com/wavetermdev/thenextwave/pkg/wlayout" "github.com/wavetermdev/thenextwave/pkg/wps" "github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient" @@ -262,10 +263,11 @@ func sendWStoreUpdatesToEventBus(updates waveobj.UpdatesRtnType) { func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.CommandCreateBlockData) (*waveobj.ORef, error) { ctx = waveobj.ContextWithUpdates(ctx) tabId := data.TabId - blockRef, err := wcore.CreateBlock(ctx, data) + blockData, err := wcore.CreateBlock(ctx, tabId, data.BlockDef, data.RtOpts) if err != nil { return nil, fmt.Errorf("error creating block: %w", err) } + blockRef := &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID} updates := waveobj.ContextGetUpdatesRtn(ctx) sendWStoreUpdatesToEventBus(updates) windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) @@ -275,14 +277,10 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command if windowId == "" { return nil, fmt.Errorf("no window found for tab") } - eventbus.SendEventToWindow(windowId, eventbus.WSEventType{ - EventType: eventbus.WSEvent_LayoutAction, - Data: &waveobj.LayoutActionData{ - ActionType: "insert", - TabId: tabId, - BlockId: blockRef.OID, - Magnified: data.Magnified, - }, + wlayout.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{ + ActionType: wlayout.LayoutActionDataType_Insert, + BlockId: blockRef.OID, + Magnified: &data.Magnified, }) return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockRef.OID}, nil } @@ -428,13 +426,9 @@ func (ws *WshServer) DeleteBlockCommand(ctx context.Context, data wshrpc.Command if err != nil { return fmt.Errorf("error deleting block: %w", err) } - eventbus.SendEventToWindow(windowId, eventbus.WSEventType{ - EventType: eventbus.WSEvent_LayoutAction, - Data: &waveobj.LayoutActionData{ - ActionType: "delete", - TabId: tabId, - BlockId: data.BlockId, - }, + wlayout.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{ + ActionType: wlayout.LayoutActionDataType_Remove, + BlockId: data.BlockId, }) updates := waveobj.ContextGetUpdatesRtn(ctx) sendWStoreUpdatesToEventBus(updates)