Add a dashboard update action that installs available Fusion releases directly from the UI. - Add an install-update API route backed by npm global install with bin-collision retry handling. - Add Update now controls, loading/error/success states, and responsive styling to the update banner and settings modal. - Extend localization, documentation, changeset coverage, and update-check tests for the new install flow. Files changed: .changeset/fn-6386-update-now-button.md | 5 + docs/dashboard-guide.md | 4 + docs/settings-reference.md | 2 + packages/dashboard/app/api/legacy.ts | 13 ++ .../dashboard/app/components/SettingsModal.css | 38 +++++- .../dashboard/app/components/SettingsModal.tsx | 96 ++++++++++++-- .../app/components/UpdateAvailableBanner.css | 46 +++++++ .../app/components/UpdateAvailableBanner.tsx | 103 ++++++++++++--- .../components/__tests__/SettingsModal.test.tsx | 79 +++++++++++ .../__tests__/UpdateAvailableBanner.test.tsx | 57 +++++++- .../components/__tests__/settings-mobile.test.tsx | 19 +++ .../src/__tests__/update-check-route.test.ts | 78 ++++++++++- .../dashboard/src/__tests__/update-check.test.ts | 45 +++++++ .../src/routes/register-update-check-routes.ts | 25 +++- packages/dashboard/src/update-check.ts | 93 +++++++++++++ packages/i18n/locales/en/app.json | 14 +- packages/i18n/src/resources.d.ts | 146 ++++++++++++++++++++- 17 files changed, 819 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-6386 Fusion-Task-Lineage: bcccf253-c0f1-493e-b849-b189a65d1479
108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
import "./UpdateAvailableBanner.css";
|
|
import { useState } from "react";
|
|
import { RefreshCw, X } from "lucide-react";
|
|
import { useTranslation, Trans } from "react-i18next";
|
|
import { getErrorMessage } from "@fusion/core";
|
|
import { installUpdate } from "../api";
|
|
import type { UpdateInstallResponse } from "../api";
|
|
|
|
interface UpdateAvailableBannerProps {
|
|
latestVersion: string;
|
|
currentVersion: string;
|
|
onDismiss: () => void;
|
|
}
|
|
|
|
export function UpdateAvailableBanner({ latestVersion, currentVersion, onDismiss }: UpdateAvailableBannerProps) {
|
|
const { t } = useTranslation("app");
|
|
const [installLoading, setInstallLoading] = useState(false);
|
|
const [installResult, setInstallResult] = useState<UpdateInstallResponse | null>(null);
|
|
|
|
const handleInstallUpdate = async () => {
|
|
setInstallLoading(true);
|
|
setInstallResult(null);
|
|
|
|
try {
|
|
setInstallResult(await installUpdate());
|
|
} catch (error) {
|
|
setInstallResult({
|
|
currentVersion,
|
|
latestVersion,
|
|
updated: false,
|
|
error: getErrorMessage(error) || t("updateBanner.updateFailed", "Update failed"),
|
|
});
|
|
} finally {
|
|
setInstallLoading(false);
|
|
}
|
|
};
|
|
|
|
const installSucceeded = installResult?.updated === true;
|
|
const installError = installResult?.error;
|
|
|
|
return (
|
|
<div className="update-available-banner" role="status" aria-live="polite">
|
|
<div className="update-available-banner__content">
|
|
<p className="update-available-banner__text">
|
|
<Trans
|
|
i18nKey="app:updateBanner.message"
|
|
defaults="Update available: v{{latestVersion}} (current: v{{currentVersion}}). Run <code>fn update</code> for an installed CLI, or pull this source checkout."
|
|
values={{ latestVersion, currentVersion }}
|
|
components={{ code: <code /> }}
|
|
/>{" "}
|
|
<a
|
|
className="update-available-banner__link"
|
|
href="https://github.com/Runfusion/Fusion/blob/main/CHANGELOG.md"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
{t("updateBanner.releaseNotes", "Release notes")}
|
|
</a>{" "}
|
|
·{" "}
|
|
<a className="update-available-banner__link" href="https://runfusion.ai" target="_blank" rel="noreferrer">
|
|
{t("updateBanner.learnMore", "Learn more")}
|
|
</a>
|
|
</p>
|
|
<div className="update-available-banner__actions">
|
|
{installSucceeded ? (
|
|
<span className="update-available-banner__install-status update-available-banner__install-status--success" aria-live="polite">
|
|
{t("updateBanner.updateSuccess", "Updated to v{{version}} — restart Fusion to apply", {
|
|
version: installResult.latestVersion ?? latestVersion,
|
|
})}
|
|
</span>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
className="btn btn-sm update-available-banner__update-btn"
|
|
onClick={() => {
|
|
void handleInstallUpdate();
|
|
}}
|
|
disabled={installLoading}
|
|
>
|
|
{installLoading ? (
|
|
<>
|
|
<RefreshCw size={12} className="spinning" aria-hidden="true" />
|
|
{t("updateBanner.updating", "Updating…")}
|
|
</>
|
|
) : (
|
|
t("updateBanner.updateNow", "Update now")
|
|
)}
|
|
</button>
|
|
)}
|
|
{installError && (
|
|
<span className="update-available-banner__install-status update-available-banner__install-status--error" aria-live="polite">
|
|
{t("updateBanner.updateFailedWithMessage", "Update failed: {{message}}", { message: installError })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
className="update-available-banner__dismiss touch-target"
|
|
aria-label={t("updateBanner.dismissLabel", "Dismiss update notice")}
|
|
onClick={onDismiss}
|
|
>
|
|
<X size={16} aria-hidden="true" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|