Establish wlayout for coordinating backend layout actions (#282)

This commit is contained in:
Evan Simkowitz 2024-08-27 18:38:57 -07:00 committed by GitHub
parent ee0bc0a377
commit c9c555452a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 294 additions and 237 deletions

View File

@ -7,13 +7,11 @@ import {
getLayoutModelForTabById, getLayoutModelForTabById,
LayoutTreeActionType, LayoutTreeActionType,
LayoutTreeInsertNodeAction, LayoutTreeInsertNodeAction,
LayoutTreeInsertNodeAtIndexAction,
newLayoutNode, newLayoutNode,
} from "@/layout/index"; } from "@/layout/index";
import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints"; import { getWebServerEndpoint, getWSServerEndpoint } from "@/util/endpoints";
import { fetch } from "@/util/fetchutil"; import { fetch } from "@/util/fetchutil";
import * as util from "@/util/util"; import * as util from "@/util/util";
import { fireAndForget } from "@/util/util";
import * as jotai from "jotai"; import * as jotai from "jotai";
import * as rxjs from "rxjs"; import * as rxjs from "rxjs";
import { modalsModel } from "./modalmodel"; import { modalsModel } from "./modalmodel";
@ -360,56 +358,6 @@ function handleWSEventMessage(msg: WSEventType) {
handleIncomingRpcMessage(rpcMsg, handleWaveEvent); handleIncomingRpcMessage(rpcMsg, handleWaveEvent);
return; 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 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 // we don't use getORefSubject here because we don't want to create a new subject
const eventSubject = eventSubjects.get(msg.eventtype); const eventSubject = eventSubjects.get(msg.eventtype);

View File

@ -26,9 +26,6 @@ class ClientServiceType {
AgreeTos(): Promise<void> { AgreeTos(): Promise<void> {
return WOS.callBackendService("client", "AgreeTos", Array.from(arguments)) return WOS.callBackendService("client", "AgreeTos", Array.from(arguments))
} }
BootstrapStarterLayout(): Promise<void> {
return WOS.callBackendService("client", "BootstrapStarterLayout", Array.from(arguments))
}
FocusWindow(arg2: string): Promise<void> { FocusWindow(arg2: string): Promise<void> {
return WOS.callBackendService("client", "FocusWindow", Array.from(arguments)) return WOS.callBackendService("client", "FocusWindow", Array.from(arguments))
} }
@ -103,9 +100,6 @@ class ObjectServiceType {
CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> { CreateBlock(blockDef: BlockDef, rtOpts: RuntimeOpts): Promise<string> {
return WOS.callBackendService("object", "CreateBlock", Array.from(arguments)) 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 // @returns object updates
DeleteBlock(blockId: string): Promise<void> { DeleteBlock(blockId: string): Promise<void> {

View File

@ -29,6 +29,7 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
rootNode: layoutStateData?.rootnode, rootNode: layoutStateData?.rootnode,
focusedNodeId: layoutStateData?.focusednodeid, focusedNodeId: layoutStateData?.focusednodeid,
magnifiedNodeId: layoutStateData?.magnifiednodeid, magnifiedNodeId: layoutStateData?.magnifiednodeid,
pendingBackendActions: layoutStateData?.pendingbackendactions,
generation: get(generationAtom), generation: get(generationAtom),
}; };
return layoutTreeState; return layoutTreeState;
@ -41,6 +42,10 @@ export function withLayoutTreeStateAtomFromTab(tabAtom: Atom<Tab>): WritableLayo
waveObjVal.rootnode = value.rootNode; waveObjVal.rootnode = value.rootNode;
waveObjVal.magnifiednodeid = value.magnifiedNodeId; waveObjVal.magnifiednodeid = value.magnifiedNodeId;
waveObjVal.focusednodeid = value.focusedNodeId; 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(generationAtom, value.generation);
set(stateAtom, waveObjVal); set(stateAtom, waveObjVal);
} }

View File

@ -6,7 +6,7 @@ import { Atom, atom, Getter, PrimitiveAtom, Setter } from "jotai";
import { splitAtom } from "jotai/utils"; import { splitAtom } from "jotai/utils";
import { createRef, CSSProperties } from "react"; import { createRef, CSSProperties } from "react";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import { balanceNode, findNode, walkNodes } from "./layoutNode"; import { balanceNode, findNode, newLayoutNode, walkNodes } from "./layoutNode";
import { import {
computeMoveNode, computeMoveNode,
deleteNode, 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. * 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. * @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); 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. // 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 ( if (
force || force ||
!this.treeState?.rootNode || !this.treeState?.rootNode ||
!this.treeState?.generation || !this.treeState?.generation ||
treeState?.generation > this.treeState.generation treeState?.generation > this.treeState.generation ||
treeState?.pendingBackendActions?.length
) { ) {
this.treeState = treeState; 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, this.leafs,
newLeafs.sort((a, b) => a.id.localeCompare(b.id)) newLeafs.sort((a, b) => a.id.localeCompare(b.id))
); );
const newLeafOrder = getLeafOrder(newLeafs, newAdditionalProps); this.treeState.leafOrder = getLeafOrder(newLeafs, newAdditionalProps);
this.setter(this.leafOrder, newLeafOrder); this.setter(this.leafOrder, this.treeState.leafOrder);
this.validateFocusedNode(newLeafOrder); this.validateFocusedNode(this.treeState.leafOrder);
this.cleanupNodeModels(); this.cleanupNodeModels();
} }
}; };

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import { atoms, globalStore, WOS } from "@/app/store/global"; import { atoms, globalStore, WOS } from "@/app/store/global";
import { fireAndForget } from "@/util/util";
import useResizeObserver from "@react-hook/resize-observer"; import useResizeObserver from "@react-hook/resize-observer";
import { Atom, useAtomValue } from "jotai"; import { Atom, useAtomValue } from "jotai";
import { CSSProperties, useEffect, useState } from "react"; import { CSSProperties, useEffect, useState } from "react";
@ -23,7 +24,7 @@ export function getLayoutModelForTab(tabAtom: Atom<Tab>): LayoutModel {
} }
const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom); const layoutTreeStateAtom = withLayoutTreeStateAtomFromTab(tabAtom);
const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set); const layoutModel = new LayoutModel(layoutTreeStateAtom, globalStore.get, globalStore.set);
globalStore.sub(layoutTreeStateAtom, () => layoutModel.updateTreeState()); globalStore.sub(layoutTreeStateAtom, () => fireAndForget(() => layoutModel.updateTreeState()));
layoutModelMap.set(tabId, layoutModel); layoutModelMap.set(tabId, layoutModel);
return layoutModel; return layoutModel;
} }

View File

@ -249,6 +249,11 @@ export type LayoutTreeState = {
rootNode: LayoutNode; rootNode: LayoutNode;
focusedNodeId?: string; focusedNodeId?: string;
magnifiedNodeId?: 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; generation: number;
}; };

View File

@ -209,7 +209,6 @@ declare global {
// waveobj.LayoutActionData // waveobj.LayoutActionData
type LayoutActionData = { type LayoutActionData = {
tabid: string;
actiontype: string; actiontype: string;
blockid: string; blockid: string;
nodesize?: number; nodesize?: number;
@ -222,6 +221,8 @@ declare global {
rootnode?: any; rootnode?: any;
magnifiednodeid?: string; magnifiednodeid?: string;
focusednodeid?: string; focusednodeid?: string;
leaforder?: string[];
pendingbackendactions?: LayoutActionData[];
}; };
// waveobj.MetaTSType // waveobj.MetaTSType

View File

@ -51,11 +51,6 @@ type WindowWatchData struct {
WatchedORefs map[waveobj.ORef]bool WatchedORefs map[waveobj.ORef]bool
} }
const (
WSLayoutActionType_Insert = "insert"
WSLayoutActionType_Remove = "delete"
)
var globalLock = &sync.Mutex{} var globalLock = &sync.Mutex{}
var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData var wsMap = make(map[string]*WindowWatchData) // websocketid => WindowWatchData

View File

@ -6,15 +6,13 @@ package clientservice
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"time" "time"
"github.com/wavetermdev/thenextwave/pkg/eventbus"
"github.com/wavetermdev/thenextwave/pkg/remote/conncontroller" "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/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wcore" "github.com/wavetermdev/thenextwave/pkg/wcore"
"github.com/wavetermdev/thenextwave/pkg/wlayout"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -97,101 +95,6 @@ func (cs *ClientService) AgreeTos(ctx context.Context) (waveobj.UpdatesRtnType,
if err != nil { if err != nil {
return nil, fmt.Errorf("error updating client data: %w", err) return nil, fmt.Errorf("error updating client data: %w", err)
} }
cs.BootstrapStarterLayout(ctx) wlayout.BootstrapStarterLayout(ctx)
return waveobj.ContextGetUpdatesRtn(ctx), nil 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
}

View File

@ -14,6 +14,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta" "github.com/wavetermdev/thenextwave/pkg/tsgen/tsgenmeta"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wcore" "github.com/wavetermdev/thenextwave/pkg/wcore"
"github.com/wavetermdev/thenextwave/pkg/wlayout"
"github.com/wavetermdev/thenextwave/pkg/wstore" "github.com/wavetermdev/thenextwave/pkg/wstore"
) )
@ -87,6 +88,11 @@ func (svc *ObjectService) AddTabToWorkspace(uiContext waveobj.UIContext, tabName
if err != nil { if err != nil {
return "", nil, fmt.Errorf("error creating tab: %w", err) 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 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) { func (svc *ObjectService) CreateBlock(uiContext waveobj.UIContext, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (string, waveobj.UpdatesRtnType, error) {
if uiContext.ActiveTabId == "" { if uiContext.ActiveTabId == "" {
return "", nil, fmt.Errorf("no active tab") return "", nil, fmt.Errorf("no active tab")
@ -193,7 +183,7 @@ func (svc *ObjectService) CreateBlock(uiContext waveobj.UIContext, blockDef *wav
defer cancelFn() defer cancelFn()
ctx = waveobj.ContextWithUpdates(ctx) 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 { if err != nil {
return "", nil, err return "", nil, err
} }

View File

@ -15,6 +15,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/util/utilfn" "github.com/wavetermdev/thenextwave/pkg/util/utilfn"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wcore" "github.com/wavetermdev/thenextwave/pkg/wcore"
"github.com/wavetermdev/thenextwave/pkg/wlayout"
"github.com/wavetermdev/thenextwave/pkg/wstore" "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) { func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId string, blockId string) (waveobj.UpdatesRtnType, error) {
log.Printf("MoveBlockToNewWindow(%s, %s)", currentTabId, blockId) log.Printf("MoveBlockToNewWindow(%s, %s)", currentTabId, blockId)
ctx = waveobj.ContextWithUpdates(ctx) 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) tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, currentTabId)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting tab: %w", err) return nil, fmt.Errorf("error getting tab: %w", err)
@ -135,21 +132,13 @@ func (svc *WindowService) MoveBlockToNewWindow(ctx context.Context, currentTabId
if !windowCreated { if !windowCreated {
return nil, fmt.Errorf("new window not created") return nil, fmt.Errorf("new window not created")
} }
eventbus.SendEventToWindow(curWindowId, eventbus.WSEventType{ wlayout.QueueLayoutActionForTab(ctx, currentTabId, waveobj.LayoutActionData{
EventType: eventbus.WSEvent_LayoutAction, ActionType: wlayout.LayoutActionDataType_Remove,
Data: waveobj.LayoutActionData{ BlockId: blockId,
ActionType: eventbus.WSLayoutActionType_Remove,
TabId: currentTabId,
BlockId: blockId,
},
}) })
eventbus.SendEventToWindow(newWindow.OID, eventbus.WSEventType{ wlayout.QueueLayoutActionForTab(ctx, newWindow.ActiveTabId, waveobj.LayoutActionData{
EventType: eventbus.WSEvent_LayoutAction, ActionType: wlayout.LayoutActionDataType_Insert,
Data: waveobj.LayoutActionData{ BlockId: blockId,
ActionType: eventbus.WSLayoutActionType_Insert,
TabId: newWindow.ActiveTabId,
BlockId: blockId,
},
}) })
return waveobj.ContextGetUpdatesRtn(ctx), nil return waveobj.ContextGetUpdatesRtn(ctx), nil
} }

View File

@ -174,21 +174,22 @@ func (t *Tab) GetBlockORefs() []ORef {
} }
type LayoutActionData struct { type LayoutActionData struct {
TabId string `json:"tabid"`
ActionType string `json:"actiontype"` ActionType string `json:"actiontype"`
BlockId string `json:"blockid"` BlockId string `json:"blockid"`
NodeSize uint `json:"nodesize,omitempty"` NodeSize *uint `json:"nodesize,omitempty"`
IndexArr []int `json:"indexarr,omitempty"` IndexArr *[]int `json:"indexarr,omitempty"`
Magnified bool `json:"magnified,omitempty"` Magnified *bool `json:"magnified,omitempty"`
} }
type LayoutState struct { type LayoutState struct {
OID string `json:"oid"` OID string `json:"oid"`
Version int `json:"version"` Version int `json:"version"`
RootNode any `json:"rootnode,omitempty"` RootNode any `json:"rootnode,omitempty"`
MagnifiedNodeId string `json:"magnifiednodeid,omitempty"` MagnifiedNodeId string `json:"magnifiednodeid,omitempty"`
FocusedNodeId string `json:"focusednodeid,omitempty"` FocusedNodeId string `json:"focusednodeid,omitempty"`
Meta MetaMapType `json:"meta,omitempty"` LeafOrder *[]string `json:"leaforder,omitempty"`
PendingBackendActions *[]LayoutActionData `json:"pendingbackendactions,omitempty"`
Meta MetaMapType `json:"meta,omitempty"`
} }
func (*LayoutState) GetOType() string { func (*LayoutState) GetOType() string {

View File

@ -168,18 +168,18 @@ func CreateClient(ctx context.Context) (*waveobj.Client, error) {
return client, nil return client, nil
} }
func CreateBlock(ctx context.Context, createBlockCmd wshrpc.CommandCreateBlockData) (*waveobj.ORef, error) { func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
tabId := createBlockCmd.TabId blockData, err := wstore.CreateBlock(ctx, tabId, blockDef, rtOpts)
blockData, err := wstore.CreateBlock(ctx, tabId, createBlockCmd.BlockDef, createBlockCmd.RtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return nil, fmt.Errorf("error creating block: %w", err)
} }
controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "") controllerName := blockData.Meta.GetString(waveobj.MetaKey_Controller, "")
if controllerName != "" { if controllerName != "" {
err = blockcontroller.StartBlockController(ctx, createBlockCmd.TabId, blockData.OID) err = blockcontroller.StartBlockController(ctx, tabId, blockData.OID)
if err != nil { if err != nil {
return nil, fmt.Errorf("error starting block controller: %w", err) return nil, fmt.Errorf("error starting block controller: %w", err)
} }
} }
return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}, nil
return blockData, nil
} }

176
pkg/wlayout/wlayout.go Normal file
View File

@ -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
}

View File

@ -22,6 +22,7 @@ import (
"github.com/wavetermdev/thenextwave/pkg/waveai" "github.com/wavetermdev/thenextwave/pkg/waveai"
"github.com/wavetermdev/thenextwave/pkg/waveobj" "github.com/wavetermdev/thenextwave/pkg/waveobj"
"github.com/wavetermdev/thenextwave/pkg/wcore" "github.com/wavetermdev/thenextwave/pkg/wcore"
"github.com/wavetermdev/thenextwave/pkg/wlayout"
"github.com/wavetermdev/thenextwave/pkg/wps" "github.com/wavetermdev/thenextwave/pkg/wps"
"github.com/wavetermdev/thenextwave/pkg/wshrpc" "github.com/wavetermdev/thenextwave/pkg/wshrpc"
"github.com/wavetermdev/thenextwave/pkg/wshrpc/wshclient" "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) { func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.CommandCreateBlockData) (*waveobj.ORef, error) {
ctx = waveobj.ContextWithUpdates(ctx) ctx = waveobj.ContextWithUpdates(ctx)
tabId := data.TabId tabId := data.TabId
blockRef, err := wcore.CreateBlock(ctx, data) blockData, err := wcore.CreateBlock(ctx, tabId, data.BlockDef, data.RtOpts)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating block: %w", err) return nil, fmt.Errorf("error creating block: %w", err)
} }
blockRef := &waveobj.ORef{OType: waveobj.OType_Block, OID: blockData.OID}
updates := waveobj.ContextGetUpdatesRtn(ctx) updates := waveobj.ContextGetUpdatesRtn(ctx)
sendWStoreUpdatesToEventBus(updates) sendWStoreUpdatesToEventBus(updates)
windowId, err := wstore.DBFindWindowForTabId(ctx, tabId) windowId, err := wstore.DBFindWindowForTabId(ctx, tabId)
@ -275,14 +277,10 @@ func (ws *WshServer) CreateBlockCommand(ctx context.Context, data wshrpc.Command
if windowId == "" { if windowId == "" {
return nil, fmt.Errorf("no window found for tab") return nil, fmt.Errorf("no window found for tab")
} }
eventbus.SendEventToWindow(windowId, eventbus.WSEventType{ wlayout.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{
EventType: eventbus.WSEvent_LayoutAction, ActionType: wlayout.LayoutActionDataType_Insert,
Data: &waveobj.LayoutActionData{ BlockId: blockRef.OID,
ActionType: "insert", Magnified: &data.Magnified,
TabId: tabId,
BlockId: blockRef.OID,
Magnified: data.Magnified,
},
}) })
return &waveobj.ORef{OType: waveobj.OType_Block, OID: blockRef.OID}, nil 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 { if err != nil {
return fmt.Errorf("error deleting block: %w", err) return fmt.Errorf("error deleting block: %w", err)
} }
eventbus.SendEventToWindow(windowId, eventbus.WSEventType{ wlayout.QueueLayoutActionForTab(ctx, tabId, waveobj.LayoutActionData{
EventType: eventbus.WSEvent_LayoutAction, ActionType: wlayout.LayoutActionDataType_Remove,
Data: &waveobj.LayoutActionData{ BlockId: data.BlockId,
ActionType: "delete",
TabId: tabId,
BlockId: data.BlockId,
},
}) })
updates := waveobj.ContextGetUpdatesRtn(ctx) updates := waveobj.ContextGetUpdatesRtn(ctx)
sendWStoreUpdatesToEventBus(updates) sendWStoreUpdatesToEventBus(updates)