Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ class RpcApiType {
return client.wshRpcCall("listallappfiles", data, opts);
}

// command "listallapps" [call]
ListAllAppsCommand(client: WshClient, opts?: RpcOpts): Promise<AppInfo[]> {
return client.wshRpcCall("listallapps", null, opts);
}

// command "listalleditableapps" [call]
ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise<AppInfo[]> {
return client.wshRpcCall("listalleditableapps", null, opts);
Expand Down
194 changes: 191 additions & 3 deletions frontend/app/workspace/widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { atoms, createBlock, getApi, isDev } from "@/store/global";
import { fireAndForget, isBlank, makeIconClass } from "@/util/util";
import {
FloatingPortal,
autoUpdate,
offset,
shift,
useDismiss,
useFloating,
useInteractions,
} from "@floating-ui/react";
import clsx from "clsx";
import { useAtomValue } from "jotai";
import { memo, useCallback, useEffect, useRef, useState } from "react";
Expand Down Expand Up @@ -67,6 +76,132 @@ const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal
);
});

function calculateGridSize(appCount: number): number {
if (appCount <= 4) return 2;
if (appCount <= 9) return 3;
if (appCount <= 16) return 4;
if (appCount <= 25) return 5;
return 6;
}

const AppsFloatingWindow = memo(
({
isOpen,
onClose,
referenceElement,
}: {
isOpen: boolean;
onClose: () => void;
referenceElement: HTMLElement;
}) => {
const [apps, setApps] = useState<AppInfo[]>([]);
const [loading, setLoading] = useState(true);

const { refs, floatingStyles, context } = useFloating({
open: isOpen,
onOpenChange: onClose,
placement: "left-start",
middleware: [offset(-2), shift({ padding: 12 })],
whileElementsMounted: autoUpdate,
elements: {
reference: referenceElement,
},
});

const dismiss = useDismiss(context);
const { getFloatingProps } = useInteractions([dismiss]);

useEffect(() => {
if (!isOpen) return;

const fetchApps = async () => {
setLoading(true);
try {
const allApps = await RpcApi.ListAllAppsCommand(TabRpcClient);
const localApps = allApps
.filter((app) => !app.appid.startsWith("draft/"))
.sort((a, b) => {
const aName = a.appid.replace(/^local\//, "");
const bName = b.appid.replace(/^local\//, "");
return aName.localeCompare(bName);
});
setApps(localApps);
} catch (error) {
console.error("Failed to fetch apps:", error);
setApps([]);
} finally {
setLoading(false);
}
};

fetchApps();
}, [isOpen]);

if (!isOpen) return null;

const gridSize = calculateGridSize(apps.length);

return (
<FloatingPortal>
<div
ref={refs.setFloating}
style={floatingStyles}
{...getFloatingProps()}
className="bg-modalbg border border-border rounded-lg shadow-xl p-4 z-50"
>
{loading ? (
<div className="flex items-center justify-center p-8">
<i className="fa fa-solid fa-spinner fa-spin text-2xl text-muted"></i>
</div>
) : apps.length === 0 ? (
<div className="text-muted text-sm p-4 text-center">No local apps found</div>
) : (
<div
className="grid gap-3"
style={{
gridTemplateColumns: `repeat(${gridSize}, minmax(0, 1fr))`,
maxWidth: `${gridSize * 80}px`,
}}
>
{apps.map((app) => {
const appMeta = app.manifest?.appmeta;
const displayName = app.appid.replace(/^local\//, "");
const icon = appMeta?.icon || "cube";
const iconColor = appMeta?.iconcolor || "white";

return (
<div
key={app.appid}
className="flex flex-col items-center justify-center p-2 rounded hover:bg-hoverbg cursor-pointer transition-colors"
onClick={() => {
const blockDef: BlockDef = {
meta: {
view: "tsunami",
controller: "tsunami",
"tsunami:appid": app.appid,
},
};
createBlock(blockDef);
onClose();
}}
>
<div style={{ color: iconColor }} className="text-3xl mb-1">
<i className={makeIconClass(icon, false)}></i>
</div>
<div className="text-xxs text-center text-secondary break-words w-full px-1">
{displayName}
</div>
</div>
);
})}
</div>
)}
</div>
</FloatingPortal>
);
}
);

const Widgets = memo(() => {
const fullConfig = useAtomValue(atoms.fullConfigAtom);
const hasCustomAIPresets = useAtomValue(atoms.hasCustomAIPresetsAtom);
Expand Down Expand Up @@ -100,6 +235,9 @@ const Widgets = memo(() => {
: Object.fromEntries(Object.entries(widgetsMap).filter(([key]) => key !== "defwidget@ai"));
const widgets = sortByDisplayOrder(filteredWidgets);

const [isAppsOpen, setIsAppsOpen] = useState(false);
const appsButtonRef = useRef<HTMLDivElement>(null);

const checkModeNeeded = useCallback(() => {
if (!containerRef.current || !measurementRef.current) return;

Expand Down Expand Up @@ -204,10 +342,27 @@ const Widgets = memo(() => {
))}
</div>
<div className="flex-grow" />
{showHelp ? (
{isDev() || showHelp ? (
<div className="grid grid-cols-2 gap-0 w-full">
<Widget key="tips" widget={tipsWidget} mode={mode} />
<Widget key="help" widget={helpWidget} mode={mode} />
{isDev() ? (
<div
ref={appsButtonRef}
className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-sm overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
onClick={() => setIsAppsOpen(!isAppsOpen)}
>
<Tooltip content="Local WaveApps" placement="left" disable={isAppsOpen}>
<div>
<i className={makeIconClass("cube", true)}></i>
</div>
</Tooltip>
</div>
) : null}
{showHelp ? (
<>
<Widget key="tips" widget={tipsWidget} mode={mode} />
<Widget key="help" widget={helpWidget} mode={mode} />
</>
) : null}
</div>
) : null}
</>
Expand All @@ -217,6 +372,24 @@ const Widgets = memo(() => {
<Widget key={`widget-${idx}`} widget={data} mode={mode} />
))}
<div className="flex-grow" />
{isDev() ? (
<div
ref={appsButtonRef}
className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-lg overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer"
onClick={() => setIsAppsOpen(!isAppsOpen)}
>
<Tooltip content="Local WaveApps" placement="left" disable={isAppsOpen}>
<div>
<i className={makeIconClass("cube", true)}></i>
</div>
{mode === "normal" && (
<div className="text-xxs mt-0.5 w-full px-0.5 text-center whitespace-nowrap overflow-hidden text-ellipsis">
apps
</div>
)}
</Tooltip>
</div>
) : null}
{showHelp ? (
<>
<Widget key="tips" widget={tipsWidget} mode={mode} />
Expand All @@ -234,6 +407,13 @@ const Widgets = memo(() => {
</div>
) : null}
</div>
{isDev() && appsButtonRef.current && (
<AppsFloatingWindow
isOpen={isAppsOpen}
onClose={() => setIsAppsOpen(false)}
referenceElement={appsButtonRef.current}
/>
)}

<div
ref={measurementRef}
Expand All @@ -249,6 +429,14 @@ const Widgets = memo(() => {
<Widget key="measurement-help" widget={helpWidget} mode="normal" />
</>
) : null}
{isDev() ? (
<div className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-lg">
<div>
<i className={makeIconClass("cube", true)}></i>
</div>
<div className="text-xxs mt-0.5 w-full px-0.5 text-center">apps</div>
</div>
) : null}
{isDev() ? (
<div
className="dev-label flex justify-center items-center w-full py-1 text-accent text-[30px]"
Expand Down
1 change: 1 addition & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ declare global {
type AppInfo = {
appid: string;
modtime: number;
manifest?: AppManifest;
};

// wshrpc.AppManifest
Expand Down
46 changes: 31 additions & 15 deletions pkg/waveappstore/waveappstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"github.com/wavetermdev/waveterm/pkg/waveapputil"
"github.com/wavetermdev/waveterm/pkg/wavebase"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/tsunami/engine"
)

const (
Expand Down Expand Up @@ -456,20 +455,20 @@ func ListAllAppFiles(appId string) (*fileutil.ReadDirResult, error) {
return fileutil.ReadDirRecursive(appDir, 10000)
}

func ListAllApps() ([]string, error) {
func ListAllApps() ([]wshrpc.AppInfo, error) {
homeDir := wavebase.GetHomeDir()
waveappsDir := filepath.Join(homeDir, "waveapps")

if _, err := os.Stat(waveappsDir); os.IsNotExist(err) {
return []string{}, nil
return []wshrpc.AppInfo{}, nil
}

namespaces, err := os.ReadDir(waveappsDir)
if err != nil {
return nil, fmt.Errorf("failed to read waveapps directory: %w", err)
}

var appIds []string
var appInfos []wshrpc.AppInfo

for _, ns := range namespaces {
if !ns.IsDir() {
Expand All @@ -493,13 +492,24 @@ func ListAllApps() ([]string, error) {
appId := MakeAppId(namespace, appName)

if err := ValidateAppId(appId); err == nil {
appIds = append(appIds, appId)
modTime, _ := GetAppModTime(appId)
appInfo := wshrpc.AppInfo{
AppId: appId,
ModTime: modTime,
}

if manifest, err := ReadAppManifest(appId); err == nil {
appInfo.Manifest = manifest
}

appInfos = append(appInfos, appInfo)
}
}
}

return appIds, nil
return appInfos, nil
}

func GetAppModTime(appId string) (int64, error) {
if err := ValidateAppId(appId); err != nil {
return 0, err
Expand Down Expand Up @@ -575,25 +585,31 @@ func ListAllEditableApps() ([]wshrpc.AppInfo, error) {
var appInfos []wshrpc.AppInfo
for appName := range allAppNames {
var appId string
var modTimeAppId string
var manifestAppId string
if localApps[appName] {
appId = MakeAppId(AppNSLocal, appName)
} else {
appId = MakeAppId(AppNSDraft, appName)
}

if draftApps[appName] {
modTimeAppId = MakeAppId(AppNSDraft, appName)
manifestAppId = MakeAppId(AppNSDraft, appName)
} else {
modTimeAppId = appId
manifestAppId = appId
}

modTime, _ := GetAppModTime(modTimeAppId)
modTime, _ := GetAppModTime(manifestAppId)

appInfos = append(appInfos, wshrpc.AppInfo{
appInfo := wshrpc.AppInfo{
AppId: appId,
ModTime: modTime,
})
}

if manifest, err := ReadAppManifest(manifestAppId); err == nil {
appInfo.Manifest = manifest
}

appInfos = append(appInfos, appInfo)
}

return appInfos, nil
Expand Down Expand Up @@ -703,7 +719,7 @@ func RenameLocalApp(appName string, newAppName string) error {
return nil
}

func ReadAppManifest(appId string) (*engine.AppManifest, error) {
func ReadAppManifest(appId string) (*wshrpc.AppManifest, error) {
if err := ValidateAppId(appId); err != nil {
return nil, fmt.Errorf("invalid appId: %w", err)
}
Expand All @@ -719,7 +735,7 @@ func ReadAppManifest(appId string) (*engine.AppManifest, error) {
return nil, fmt.Errorf("failed to read %s: %w", ManifestFileName, err)
}

var manifest engine.AppManifest
var manifest wshrpc.AppManifest
if err := json.Unmarshal(data, &manifest); err != nil {
return nil, fmt.Errorf("failed to parse %s: %w", ManifestFileName, err)
}
Expand Down Expand Up @@ -785,7 +801,7 @@ func WriteAppSecretBindings(appId string, bindings map[string]string) error {
return nil
}

func BuildAppSecretEnv(appId string, manifest *engine.AppManifest, bindings map[string]string) (map[string]string, error) {
func BuildAppSecretEnv(appId string, manifest *wshrpc.AppManifest, bindings map[string]string) (map[string]string, error) {
if manifest == nil {
return make(map[string]string), nil
}
Expand Down
Loading
Loading