fe now rendering workspace/tab from db. got useWaveObject working. need to work on updates

This commit is contained in:
sawka 2024-05-27 00:47:10 -07:00
parent 95ce1cc86d
commit 6d3f76cb74
9 changed files with 145 additions and 127 deletions

View File

@ -3,7 +3,7 @@
import * as React from "react"; import * as React from "react";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { atoms, blockDataMap, removeBlockFromTab } from "@/store/global"; import { atoms, blockDataMap } from "@/store/global";
import { TerminalView } from "@/app/view/term"; import { TerminalView } from "@/app/view/term";
import { PreviewView } from "@/app/view/preview"; import { PreviewView } from "@/app/view/preview";
@ -17,7 +17,7 @@ const Block = ({ tabId, blockId }: { tabId: string; blockId: string }) => {
const [dims, setDims] = React.useState({ width: 0, height: 0 }); const [dims, setDims] = React.useState({ width: 0, height: 0 });
function handleClose() { function handleClose() {
removeBlockFromTab(tabId, blockId); // TODO
} }
React.useEffect(() => { React.useEffect(() => {

View File

@ -15,28 +15,40 @@ import * as wstore from "@/gopkg/wstore";
import { Call as $Call } from "@wailsio/runtime"; import { Call as $Call } from "@wailsio/runtime";
const globalStore = jotai.createStore(); const globalStore = jotai.createStore();
const tabId1 = uuidv4();
const tabArr: wstore.Tab[] = [new wstore.Tab({ name: "Tab 1", tabid: tabId1, blockids: [] })];
const blockDataMap = new Map<string, jotai.Atom<wstore.Block>>(); const blockDataMap = new Map<string, jotai.Atom<wstore.Block>>();
const blockAtomCache = new Map<string, Map<string, jotai.Atom<any>>>();
const atoms = { const atoms = {
activeTabId: jotai.atom<string>(tabId1),
tabsAtom: jotai.atom<wstore.Tab[]>(tabArr),
blockDataMap: blockDataMap, blockDataMap: blockDataMap,
clientAtom: jotai.atom(null) as jotai.PrimitiveAtom<wstore.Client>, clientAtom: jotai.atom(null) as jotai.PrimitiveAtom<wstore.Client>,
// initialized in wave.ts (will not be null inside of application) // initialized in wave.ts (will not be null inside of application)
windowId: jotai.atom<string>(null) as jotai.PrimitiveAtom<string>, windowId: jotai.atom<string>(null) as jotai.PrimitiveAtom<string>,
windowData: jotai.atom<wstore.Window>(null) as jotai.PrimitiveAtom<wstore.Window>, windowData: jotai.atom<WaveWindow>(null) as jotai.PrimitiveAtom<WaveWindow>,
}; };
type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void }; type SubjectWithRef<T> = rxjs.Subject<T> & { refCount: number; release: () => void };
const blockSubjects = new Map<string, SubjectWithRef<any>>(); const blockSubjects = new Map<string, SubjectWithRef<any>>();
function isBlank(str: string): boolean {
return str == null || str == "";
}
function makeORef(otype: string, oid: string): string {
if (isBlank(otype) || isBlank(oid)) {
return null;
}
return `${otype}:${oid}`;
}
function splitORef(oref: string): [string, string] {
let parts = oref.split(":");
if (parts.length != 2) {
throw new Error("invalid oref");
}
return [parts[0], parts[1]];
}
function getBlockSubject(blockId: string): SubjectWithRef<any> { function getBlockSubject(blockId: string): SubjectWithRef<any> {
let subject = blockSubjects.get(blockId); let subject = blockSubjects.get(blockId);
if (subject == null) { if (subject == null) {
@ -69,20 +81,6 @@ Events.On("block:ptydata", (event: any) => {
subject.next(data); subject.next(data);
}); });
function addBlockIdToTab(tabId: string, blockId: string) {
let tabArr = globalStore.get(atoms.tabsAtom);
const newTabArr = produce(tabArr, (draft) => {
const tab = draft.find((tab) => tab.tabid == tabId);
tab.blockids.push(blockId);
});
globalStore.set(atoms.tabsAtom, newTabArr);
}
function removeBlock(blockId: string) {
blockDataMap.delete(blockId);
blockAtomCache.delete(blockId);
}
function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom<T>): jotai.Atom<T> { function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom<T>): jotai.Atom<T> {
let blockCache = blockAtomCache.get(blockId); let blockCache = blockAtomCache.get(blockId);
if (blockCache == null) { if (blockCache == null) {
@ -97,18 +95,7 @@ function useBlockAtom<T>(blockId: string, name: string, makeFn: () => jotai.Atom
return atom as jotai.Atom<T>; return atom as jotai.Atom<T>;
} }
function removeBlockFromTab(tabId: string, blockId: string) { function GetObject<T>(oref: string): Promise<T> {
let tabArr = globalStore.get(atoms.tabsAtom);
const newTabArr = produce(tabArr, (draft) => {
const tab = draft.find((tab) => tab.tabid == tabId);
tab.blockids = tab.blockids.filter((id) => id !== blockId);
});
globalStore.set(atoms.tabsAtom, newTabArr);
removeBlock(blockId);
BlockService.CloseBlock(blockId);
}
function GetObject(oref: string): Promise<any> {
let prtn = $Call.ByName( let prtn = $Call.ByName(
"github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject", "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetObject",
oref oref
@ -116,84 +103,85 @@ function GetObject(oref: string): Promise<any> {
return prtn; return prtn;
} }
type WaveObjectHookData = { function GetClientObject(): Promise<Client> {
oref: string; let prtn = $Call.ByName(
}; "github.com/wavetermdev/thenextwave/pkg/service/objectservice.ObjectService.GetClientObject"
);
return prtn;
}
type WaveObjectValue<T> = { type WaveObjectValue<T> = {
pendingPromise: Promise<any>; pendingPromise: Promise<any>;
value: T; dataAtom: jotai.PrimitiveAtom<{ value: T; loading: boolean }>;
loading: boolean;
}; };
const waveObjectValueCache = new Map<string, WaveObjectValue<any>>(); const waveObjectValueCache = new Map<string, WaveObjectValue<any>>();
let waveObjectAtomCache = new WeakMap<WaveObjectHookData, jotai.Atom<any>>();
function clearWaveObjectCache() { function clearWaveObjectCache() {
waveObjectValueCache.clear(); waveObjectValueCache.clear();
waveObjectAtomCache = new WeakMap<WaveObjectHookData, jotai.Atom<any>>();
} }
function createWaveObjectAtom<T>(oref: string): jotai.Atom<[T, boolean]> { function createWaveValueObject<T>(oref: string): WaveObjectValue<T> {
let cacheVal: WaveObjectValue<T> = waveObjectValueCache.get(oref); const wov = { pendingPromise: null, dataAtom: null };
if (cacheVal == null) { wov.dataAtom = jotai.atom({ value: null, loading: true });
cacheVal = { pendingPromise: null, value: null, loading: true }; let startTs = Date.now();
cacheVal.pendingPromise = GetObject(oref).then((val) => { let localPromise = GetObject<T>(oref);
cacheVal.value = val; wov.pendingPromise = localPromise;
cacheVal.loading = false; localPromise.then((val) => {
cacheVal.pendingPromise = null; if (wov.pendingPromise != localPromise) {
}); return;
waveObjectValueCache.set(oref, cacheVal);
}
return jotai.atom(
(get) => {
return [cacheVal.value, cacheVal.loading];
},
(get, set, newVal: T) => {
cacheVal.value = newVal;
} }
); const [otype, oid] = splitORef(oref);
if (val != null) {
if (val["otype"] != otype) {
throw new Error("GetObject returned wrong type");
}
if (val["oid"] != oid) {
throw new Error("GetObject returned wrong id");
}
}
wov.pendingPromise = null;
globalStore.set(wov.dataAtom, { value: val, loading: false });
console.log("GetObject resolved", oref, val, Date.now() - startTs + "ms");
});
return wov;
} }
function useWaveObjectValue<T>(oref: string): [T, boolean] { function useWaveObjectValue<T>(oref: string): [T, boolean] {
const objRef = React.useRef<WaveObjectHookData>(null); console.log("useWaveObjectValue", oref);
if (objRef.current == null) { let wov = waveObjectValueCache.get(oref);
objRef.current = { oref: oref }; if (wov == null) {
console.log("creating new wov", oref);
wov = createWaveValueObject(oref);
waveObjectValueCache.set(oref, wov);
} }
const objHookData = objRef.current; const atomVal = jotai.useAtomValue(wov.dataAtom);
let objAtom = waveObjectAtomCache.get(objHookData); return [atomVal.value, atomVal.loading];
if (objAtom == null) {
objAtom = createWaveObjectAtom(oref);
waveObjectAtomCache.set(objHookData, objAtom);
}
const atomVal = jotai.useAtomValue(objAtom);
return [atomVal[0], atomVal[1]];
} }
function useWaveObject<T>(oref: string): [T, boolean, (T) => void] { function useWaveObject<T>(oref: string): [T, boolean, (T) => void] {
const objRef = React.useRef<WaveObjectHookData>(null); console.log("useWaveObject", oref);
if (objRef.current == null) { let wov = waveObjectValueCache.get(oref);
objRef.current = { oref: oref }; if (wov == null) {
wov = createWaveValueObject(oref);
waveObjectValueCache.set(oref, wov);
} }
const objHookData = objRef.current; const [atomVal, setAtomVal] = jotai.useAtom(wov.dataAtom);
let objAtom = waveObjectAtomCache.get(objHookData); const simpleSet = (val: T) => {
if (objAtom == null) { setAtomVal({ value: val, loading: false });
objAtom = createWaveObjectAtom(oref); };
waveObjectAtomCache.set(objHookData, objAtom); return [atomVal.value, atomVal.loading, simpleSet];
}
const [atomVal, setAtomVal] = jotai.useAtom(objAtom);
return [atomVal[0], atomVal[1], setAtomVal];
} }
export { export {
globalStore, globalStore,
makeORef,
atoms, atoms,
getBlockSubject, getBlockSubject,
addBlockIdToTab,
blockDataMap, blockDataMap,
useBlockAtom, useBlockAtom,
removeBlockFromTab,
GetObject, GetObject,
GetClientObject,
useWaveObject, useWaveObject,
useWaveObjectValue, useWaveObjectValue,
clearWaveObjectCache, clearWaveObjectCache,

View File

@ -5,12 +5,16 @@ import * as React from "react";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { Block } from "@/app/block/block"; import { Block } from "@/app/block/block";
import { atoms } from "@/store/global"; import { atoms } from "@/store/global";
import * as gdata from "@/store/global";
import "./tab.less"; import "./tab.less";
import { CenteredLoadingDiv } from "../element/quickelems";
const TabContent = ({ tabId }: { tabId: string }) => { const TabContent = ({ tabId }: { tabId: string }) => {
const tabs = jotai.useAtomValue(atoms.tabsAtom); const [tabData, tabLoading] = gdata.useWaveObjectValue<Tab>(gdata.makeORef("tab", tabId));
const tabData = tabs.find((tab) => tab.tabid === tabId); if (tabLoading) {
return <CenteredLoadingDiv />;
}
if (!tabData) { if (!tabData) {
return <div className="tabcontent">Tab not found</div>; return <div className="tabcontent">Tab not found</div>;
} }

View File

@ -5,32 +5,40 @@ import * as React from "react";
import * as jotai from "jotai"; import * as jotai from "jotai";
import { TabContent } from "@/app/tab/tab"; import { TabContent } from "@/app/tab/tab";
import { clsx } from "clsx"; import { clsx } from "clsx";
import { atoms, addBlockIdToTab, blockDataMap } from "@/store/global"; import { atoms, blockDataMap } from "@/store/global";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { BlockService } from "@/bindings/blockservice"; import { BlockService } from "@/bindings/blockservice";
import { ClientService } from "@/bindings/clientservice"; import { ClientService } from "@/bindings/clientservice";
import { Workspace } from "@/gopkg/wstore"; import { Workspace } from "@/gopkg/wstore";
import * as wstore from "@/gopkg/wstore"; import * as wstore from "@/gopkg/wstore";
import * as jotaiUtil from "jotai/utils"; import * as jotaiUtil from "jotai/utils";
import * as gdata from "@/store/global";
import "./workspace.less"; import "./workspace.less";
import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems"; import { CenteredLoadingDiv, CenteredDiv } from "../element/quickelems";
function Tab({ tab }: { tab: wstore.Tab }) { function Tab({ tabId }: { tabId: string }) {
const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId); const windowData = jotai.useAtomValue(atoms.windowData);
const [tabData, tabLoading] = gdata.useWaveObjectValue<Tab>(gdata.makeORef("tab", tabId));
function setActiveTab(tabId: string) {
if (tabId == null) {
return;
}
// TODO
}
return ( return (
<div className={clsx("tab", { active: activeTab === tab.tabid })} onClick={() => setActiveTab(tab.tabid)}> <div
{tab.name} className={clsx("tab", { active: tabData != null && windowData.activetabid === tabData.oid })}
onClick={() => setActiveTab(tabData?.oid)}
>
{tabData?.name ?? "..."}
</div> </div>
); );
} }
function TabBar() { function TabBar({ workspace, waveWindow }: { workspace: Workspace; waveWindow: WaveWindow }) {
const [tabData, setTabData] = jotai.useAtom(atoms.tabsAtom);
const [activeTab, setActiveTab] = jotai.useAtom(atoms.activeTabId);
const tabs = jotai.useAtomValue(atoms.tabsAtom);
const client = jotai.useAtomValue(atoms.clientAtom);
function handleAddTab() { function handleAddTab() {
const newTabId = uuidv4(); const newTabId = uuidv4();
const newTabName = "Tab " + (tabData.length + 1); const newTabName = "Tab " + (tabData.length + 1);
@ -38,10 +46,11 @@ function TabBar() {
setActiveTab(newTabId); setActiveTab(newTabId);
} }
const tabIds = workspace?.tabids ?? [];
return ( return (
<div className="tab-bar"> <div className="tab-bar">
{tabs.map((tab, idx) => { {tabIds.map((tabid, idx) => {
return <Tab key={idx} tab={tab} />; return <Tab key={idx} tabId={tabid} />;
})} })}
<div className="tab-add" onClick={() => handleAddTab()}> <div className="tab-add" onClick={() => handleAddTab()}>
<i className="fa fa-solid fa-plus fa-fw" /> <i className="fa fa-solid fa-plus fa-fw" />
@ -51,7 +60,8 @@ function TabBar() {
} }
function Widgets() { function Widgets() {
const activeTabId = jotai.useAtomValue(atoms.activeTabId); const windowData = jotai.useAtomValue(atoms.windowData);
const activeTabId = windowData.activetabid;
async function createBlock(blockDef: wstore.BlockDef) { async function createBlock(blockDef: wstore.BlockDef) {
const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } }); const rtOpts: wstore.RuntimeOpts = new wstore.RuntimeOpts({ termsize: { rows: 25, cols: 80 } });
@ -113,27 +123,15 @@ function Widgets() {
function WorkspaceElem() { function WorkspaceElem() {
const windowData = jotai.useAtomValue(atoms.windowData); const windowData = jotai.useAtomValue(atoms.windowData);
const activeTabId = jotai.useAtomValue(atoms.activeTabId); const workspaceId = windowData?.workspaceid;
const workspaceId = windowData.workspaceid; const activeTabId = windowData?.activetabid;
const wsAtom = React.useMemo(() => { const [ws, wsLoading] = gdata.useWaveObjectValue<Workspace>(gdata.makeORef("workspace", workspaceId));
return jotaiUtil.loadable( if (wsLoading) {
jotai.atom(async (get) => {
const ws = await ClientService.GetWorkspace(workspaceId);
return ws;
})
);
}, [workspaceId]);
const wsLoadable = jotai.useAtomValue(wsAtom);
if (wsLoadable.state === "loading") {
return <CenteredLoadingDiv />; return <CenteredLoadingDiv />;
} }
if (wsLoadable.state === "hasError") {
return <CenteredDiv>Error: {wsLoadable.error?.toString()}</CenteredDiv>;
}
const ws: Workspace = wsLoadable.data;
return ( return (
<div className="workspace"> <div className="workspace">
<TabBar /> <TabBar workspace={ws} waveWindow={windowData} />
<div className="workspace-tabcontent"> <div className="workspace-tabcontent">
<TabContent key={workspaceId} tabId={activeTabId} /> <TabContent key={workspaceId} tabId={activeTabId} />
<Widgets /> <Widgets />

View File

@ -81,7 +81,7 @@ declare global {
oid: string; oid: string;
}; };
type Window = { type WaveWindow = {
otype: string; otype: string;
oid: string; oid: string;
version: number; version: number;

View File

@ -7,7 +7,7 @@ import { App } from "./app/app";
import { loadFonts } from "./util/fontutil"; import { loadFonts } from "./util/fontutil";
import { ClientService } from "@/bindings/clientservice"; import { ClientService } from "@/bindings/clientservice";
import { Client } from "@/gopkg/wstore"; import { Client } from "@/gopkg/wstore";
import { globalStore, atoms } from "@/store/global"; import { globalStore, atoms, GetClientObject, GetObject, makeORef } from "@/store/global";
import * as wailsRuntime from "@wailsio/runtime"; import * as wailsRuntime from "@wailsio/runtime";
import * as wstore from "@/gopkg/wstore"; import * as wstore from "@/gopkg/wstore";
import { immerable } from "immer"; import { immerable } from "immer";
@ -30,9 +30,9 @@ wstore.WinSize.prototype[immerable] = true;
loadFonts(); loadFonts();
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
const client = await ClientService.GetClientData(); const client = await GetClientObject();
globalStore.set(atoms.clientAtom, client); globalStore.set(atoms.clientAtom, client);
const window = await ClientService.GetWindow(windowId); const window = await GetObject<WaveWindow>(makeORef("window", windowId));
globalStore.set(atoms.windowData, window); globalStore.set(atoms.windowData, window);
let reactElem = React.createElement(App, null, null); let reactElem = React.createElement(App, null, null);
let elem = document.getElementById("main"); let elem = document.getElementById("main");

View File

@ -25,6 +25,16 @@ func parseORef(oref string) (*waveobj.ORef, error) {
return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil return &waveobj.ORef{OType: fields[0], OID: fields[1]}, nil
} }
func (svc *ObjectService) GetClientObject() (any, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), DefaultTimeout)
defer cancelFn()
client, err := wstore.DBGetSingleton[*wstore.Client](ctx)
if err != nil {
return nil, fmt.Errorf("error getting client: %w", err)
}
return waveobj.ToJsonMap(client)
}
func (svc *ObjectService) GetObject(orefStr string) (any, error) { func (svc *ObjectService) GetObject(orefStr string) (any, error) {
oref, err := parseORef(orefStr) oref, err := parseORef(orefStr)
if err != nil { if err != nil {
@ -36,7 +46,7 @@ func (svc *ObjectService) GetObject(orefStr string) (any, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting object: %w", err) return nil, fmt.Errorf("error getting object: %w", err)
} }
return obj, nil return waveobj.ToJsonMap(obj)
} }
func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) { func (svc *ObjectService) GetObjects(orefStrArr []string) (any, error) {

View File

@ -124,7 +124,7 @@ func SetVersion(waveObj WaveObj, version int) {
reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version)) reflect.ValueOf(waveObj).Elem().FieldByIndex(desc.VersionField.Index).SetInt(int64(version))
} }
func ToJson(w WaveObj) ([]byte, error) { func ToJsonMap(w WaveObj) (map[string]any, error) {
m := make(map[string]any) m := make(map[string]any)
dconfig := &mapstructure.DecoderConfig{ dconfig := &mapstructure.DecoderConfig{
Result: &m, Result: &m,
@ -139,6 +139,16 @@ func ToJson(w WaveObj) ([]byte, error) {
return nil, err return nil, err
} }
m[OTypeKeyName] = w.GetOType() m[OTypeKeyName] = w.GetOType()
m[OIDKeyName] = GetOID(w)
m[VersionKeyName] = GetVersion(w)
return m, nil
}
func ToJson(w WaveObj) ([]byte, error) {
m, err := ToJsonMap(w)
if err != nil {
return nil, err
}
return json.Marshal(m) return json.Marshal(m)
} }
@ -253,10 +263,18 @@ func typeToTSType(t reflect.Type) (string, []reflect.Type) {
} }
} }
var tsRenameMap = map[string]string{
"Window": "WaveWindow",
}
func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) { func generateTSTypeInternal(rtype reflect.Type) (string, []reflect.Type) {
var buf bytes.Buffer var buf bytes.Buffer
waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem() waveObjType := reflect.TypeOf((*WaveObj)(nil)).Elem()
buf.WriteString(fmt.Sprintf("type %s = {\n", rtype.Name())) tsTypeName := rtype.Name()
if tsRename, ok := tsRenameMap[tsTypeName]; ok {
tsTypeName = tsRename
}
buf.WriteString(fmt.Sprintf("type %s = {\n", tsTypeName))
var isWaveObj bool var isWaveObj bool
if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) { if rtype.Implements(waveObjType) || reflect.PointerTo(rtype).Implements(waveObjType) {
isWaveObj = true isWaveObj = true

View File

@ -209,8 +209,8 @@ func EnsureInitialData() error {
return fmt.Errorf("error inserting workspace: %w", err) return fmt.Errorf("error inserting workspace: %w", err)
} }
tab := &Tab{ tab := &Tab{
OID: uuid.New().String(), OID: tabId,
Name: "Tab 1", Name: "Tab-1",
BlockIds: []string{}, BlockIds: []string{},
} }
err = DBInsert(ctx, tab) err = DBInsert(ctx, tab)