waveterm/frontend/builder/tabs/builder-configdatatab.tsx
2025-11-24 13:37:24 -08:00

226 lines
No EOL
8.2 KiB
TypeScript

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model";
import { CopyButton } from "@/element/copybutton";
import { atoms } from "@/store/global";
import { cn } from "@/util/util";
import { useAtomValue } from "jotai";
import { memo, useCallback, useEffect, useState } from "react";
const NotRunningView = memo(() => {
return (
<div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6 max-w-[500px] text-center px-8">
<i className="fa fa-triangle-exclamation text-6xl text-warning" />
<div className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold text-primary">App Not Running</h2>
<p className="text-base text-secondary leading-relaxed">
The tsunami app must be running to view config and data. Please start the app from the Preview
tab first.
</p>
</div>
</div>
</div>
);
});
NotRunningView.displayName = "NotRunningView";
const ErrorView = memo(({ errorMsg }: { errorMsg: string }) => {
return (
<div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6 max-w-2xl text-center px-8">
<i className="fa fa-circle-xmark text-6xl text-error" />
<div className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold text-error">Error Loading Data</h2>
<div className="text-left bg-panel border border-error/30 rounded-lg p-4">
<pre className="text-sm text-secondary whitespace-pre-wrap font-mono">{errorMsg}</pre>
</div>
</div>
</div>
</div>
);
});
ErrorView.displayName = "ErrorView";
const LoadingView = memo(() => {
return (
<div className="w-full h-full flex items-center justify-center bg-background">
<div className="flex flex-col items-center gap-6">
<i className="fa fa-spinner fa-spin text-6xl text-secondary" />
<p className="text-base text-secondary">Loading data...</p>
</div>
</div>
);
});
LoadingView.displayName = "LoadingView";
type ConfigDataState = {
config: any;
data: any;
error: string | null;
isLoading: boolean;
};
const BuilderConfigDataTab = memo(() => {
const model = BuilderAppPanelModel.getInstance();
const builderStatus = useAtomValue(model.builderStatusAtom);
const builderId = useAtomValue(atoms.builderId);
const activeTab = useAtomValue(model.activeTab);
const [state, setState] = useState<ConfigDataState>({
config: null,
data: null,
error: null,
isLoading: false,
});
const isRunning = builderStatus?.status === "running" && builderStatus?.port && builderStatus.port !== 0;
const fetchData = useCallback(async () => {
if (!isRunning || !builderStatus?.port) {
return;
}
setState((prev) => ({ ...prev, isLoading: true, error: null }));
try {
const baseUrl = `http://localhost:${builderStatus.port}`;
const [configResponse, dataResponse] = await Promise.all([
fetch(`${baseUrl}/api/config`),
fetch(`${baseUrl}/api/data`),
]);
if (!configResponse.ok) {
throw new Error(`Failed to fetch config: ${configResponse.statusText}`);
}
if (!dataResponse.ok) {
throw new Error(`Failed to fetch data: ${dataResponse.statusText}`);
}
const config = await configResponse.json();
const data = await dataResponse.json();
setState({
config,
data,
error: null,
isLoading: false,
});
} catch (err) {
setState({
config: null,
data: null,
error: err instanceof Error ? err.message : String(err),
isLoading: false,
});
}
}, [isRunning, builderStatus?.port]);
const handleRefresh = useCallback(async () => {
setState({
config: null,
data: null,
error: null,
isLoading: true,
});
await new Promise((resolve) => setTimeout(resolve, 200));
await fetchData();
}, [fetchData]);
const handleCopyConfig = useCallback(() => {
if (state.config) {
navigator.clipboard.writeText(JSON.stringify(state.config, null, 2));
}
}, [state.config]);
const handleCopyData = useCallback(() => {
if (state.data) {
navigator.clipboard.writeText(JSON.stringify(state.data, null, 2));
}
}, [state.data]);
useEffect(() => {
if (activeTab === "configdata" && isRunning) {
fetchData();
} else if (!isRunning) {
setState({
config: null,
data: null,
error: null,
isLoading: false,
});
}
}, [activeTab, isRunning, fetchData]);
if (!isRunning) {
return <NotRunningView />;
}
if (state.isLoading) {
return <LoadingView />;
}
if (state.error) {
return <ErrorView errorMsg={state.error} />;
}
if (!state.config && !state.data) {
return <LoadingView />;
}
return (
<div className="w-full h-full flex flex-col bg-background">
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-border">
<h3 className="text-lg font-semibold text-primary">Config & Data</h3>
<button
onClick={handleRefresh}
className="px-3 py-1 text-sm font-medium rounded bg-accent/80 text-primary hover:bg-accent transition-colors cursor-pointer flex items-center gap-2"
>
<i className="fa fa-refresh" />
Refresh
</button>
</div>
<div className="flex-1 overflow-auto p-4">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h4 className="text-base font-semibold text-primary flex items-center gap-2">
<i className="fa fa-gear" />
Config
</h4>
<CopyButton title="Copy Config" onClick={handleCopyConfig} />
</div>
<div className="bg-panel border border-border rounded-lg p-4 overflow-auto">
<pre className="text-xs text-primary font-mono whitespace-pre">
{JSON.stringify(state.config, null, 2)}
</pre>
</div>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<h4 className="text-base font-semibold text-primary flex items-center gap-2">
<i className="fa fa-database" />
Data
</h4>
<CopyButton title="Copy Data" onClick={handleCopyData} />
</div>
<div className="bg-panel border border-border rounded-lg p-4 overflow-auto">
<pre className="text-xs text-primary font-mono whitespace-pre">
{JSON.stringify(state.data, null, 2)}
</pre>
</div>
</div>
</div>
</div>
</div>
);
});
BuilderConfigDataTab.displayName = "BuilderConfigDataTab";
export { BuilderConfigDataTab };