Skip to content
Draft
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
38 changes: 36 additions & 2 deletions client/src/protoFleet/features/settings/components/Firmware.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import DeleteAllFirmwareDialog from "@/protoFleet/features/settings/components/D
import DeleteFirmwareDialog from "@/protoFleet/features/settings/components/DeleteFirmwareDialog";
import EditFirmwareMetadataDialog from "@/protoFleet/features/settings/components/EditFirmwareMetadataDialog";
import FirmwareUploadDialog from "@/protoFleet/features/settings/components/FirmwareUploadDialog";
import ActiveUpdatesMonitor, {
type MonitorRequest,
} from "@/protoFleet/features/settings/components/ReleaseChannels/ActiveUpdatesMonitor";
import ReleaseChannelsTab from "@/protoFleet/features/settings/components/ReleaseChannels/ReleaseChannelsTab";
import SettingsEmptyState from "@/protoFleet/features/settings/components/SettingsEmptyState";
import SettingsPageHeader from "@/protoFleet/features/settings/components/SettingsPageHeader";
Expand Down Expand Up @@ -371,6 +374,13 @@ const Firmware = () => {
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get("tab") === RELEASE_CHANNELS_TAB_PARAM ? TAB_RELEASE_CHANNELS : TAB_FILES;
const channelsApi = useReleaseChannels();
// Channel an update's "Manage" action asked to open; remounts the channels
// tab so it starts on that channel.
const [manageRequest, setManageRequest] = useState<{ channelId: bigint; seq: number } | null>(null);
// Update detail / rollback the history modal asked the monitor to open.
const [monitorRequest, setMonitorRequest] = useState<MonitorRequest | null>(null);

const showChannels = () => setSearchParams({ tab: RELEASE_CHANNELS_TAB_PARAM }, { replace: true });

return (
<div className="flex flex-col gap-6">
Expand All @@ -382,10 +392,34 @@ const Firmware = () => {
segments={firmwareTabs}
initialSegmentKey={activeTab}
onSelect={(key) => {
setSearchParams(key === TAB_RELEASE_CHANNELS ? { tab: RELEASE_CHANNELS_TAB_PARAM } : {}, { replace: true });
if (key === TAB_RELEASE_CHANNELS) {
showChannels();
} else {
setManageRequest(null);
setSearchParams({}, { replace: true });
}
}}
/>
<ActiveUpdatesMonitor
api={channelsApi}
request={monitorRequest}
onRequestHandled={() => setMonitorRequest(null)}
onManageChannel={(channelId) => {
setManageRequest((current) => ({ channelId, seq: (current?.seq ?? 0) + 1 }));
showChannels();
}}
/>
{activeTab === TAB_RELEASE_CHANNELS ? <ReleaseChannelsTab api={channelsApi} /> : <FirmwareFilesSection />}
{activeTab === TAB_RELEASE_CHANNELS ? (
<ReleaseChannelsTab
key={manageRequest ? `manage-${manageRequest.seq}` : "channels"}
api={channelsApi}
initialManagedChannelId={manageRequest?.channelId ?? null}
onViewRollout={(rollout) => setMonitorRequest({ kind: "view", rolloutId: rollout.id })}
onRollbackRollout={(rollout) => setMonitorRequest({ kind: "rollback", rolloutId: rollout.id })}
/>
) : (
<FirmwareFilesSection />
)}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from "@storybook/react-vite";

import ActiveUpdateBanners from "./ActiveUpdateBanners";
import ChannelHistoryModal from "./ChannelHistoryModal";
import {
activeRigRollout,
batchedRigRollout,
canaryChannel,
canaryHistory,
gatedRigRollout,
pausedRigRollout,
} from "./ReleaseChannels.fixtures";

// Inline progress banners for ongoing firmware updates, stacked above the
// firmware page tabs; and the per-channel update history they lead to.
const meta = {
title: "Proto Fleet/Firmware/Release Channels/Active Updates",
component: ActiveUpdateBanners,
parameters: {
layout: "padded",
},
} satisfies Meta<typeof ActiveUpdateBanners>;

export default meta;

type Story = StoryObj<typeof ActiveUpdateBanners>;

const noop = () => {};

export const Stacked: Story = {
name: "Running, gated, failing and paused updates",
render: () => (
<div className="max-w-4xl">
<ActiveUpdateBanners
rollouts={[activeRigRollout, gatedRigRollout, batchedRigRollout, pausedRigRollout]}
onViewUpdate={noop}
/>
</div>
),
};

export const History: Story = {
name: "Channel update history",
render: () => (
<div className="min-h-screen bg-surface-base">
<ChannelHistoryModal
channel={canaryChannel}
rollouts={canaryHistory}
onView={noop}
onRollback={noop}
onClose={noop}
/>
</div>
),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { activeUpdateSummary, hasFailures, needsManualReview } from "./rolloutStatus";
import type { Rollout } from "@/protoFleet/api/generated/rollout/v1/rollout_pb";
import { Download } from "@/shared/assets/icons";
import Callout, { intents } from "@/shared/components/Callout";

interface ActiveUpdateBannersProps {
// Ongoing rollouts, most recently started first.
rollouts: Rollout[];
onViewUpdate: (rollout: Rollout) => void;
}

// Inline progress banners for ongoing firmware updates, one per rollout,
// stacked above the firmware page tabs so they are visible from Files and
// Release channels alike. Each opens the full update detail.
const ActiveUpdateBanners = ({ rollouts, onViewUpdate }: ActiveUpdateBannersProps) => {
if (rollouts.length === 0) return null;
return (
<div className="flex flex-col gap-3" data-testid="active-updates-section">
{rollouts.map((rollout) => (
<div key={rollout.id.toString()} data-testid={`active-update-${rollout.id.toString()}`}>
<Callout
intent={hasFailures(rollout) ? intents.danger : intents.warning}
prefixIcon={<Download className="text-text-primary" />}
title={`${rollout.channelName}, ${rollout.model} firmware update`}
subtitle={activeUpdateSummary(rollout)}
buttonText={needsManualReview(rollout) ? "Review update" : "View update"}
buttonOnClick={() => onViewUpdate(rollout)}
testId={`update-banner-${rollout.id.toString()}`}
/>
</div>
))}
</div>
);
};

export default ActiveUpdateBanners;
Loading
Loading