mirror of
https://github.com/wavetermdev/waveterm.git
synced 2024-12-21 16:38:23 +01:00
Bootstrap layout on first launch (#186)
This commit is contained in:
parent
8ebde7e766
commit
74e86ef0cc
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
2
frontend/types/gotypes.d.ts
vendored
2
frontend/types/gotypes.d.ts
vendored
@ -472,6 +472,8 @@ declare global {
|
||||
tabid: string;
|
||||
actiontype: string;
|
||||
blockid: string;
|
||||
nodesize?: number;
|
||||
indexarr?: number[];
|
||||
};
|
||||
|
||||
// webcmd.WSRpcCommand
|
||||
|
@ -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{}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user