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
84 changes: 54 additions & 30 deletions packages/web/src/components/device-detail/device-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ import {
SelectValue,
} from "@/components/ui/select";
import { deviceApi, handleApiError } from "@/lib/api";
import {
UPDATE_CHANNELS,
getChannelRelease,
hasAnyRelease,
type UpdateChannel,
} from "@/lib/firmware-updates";
import type { DeviceStatus, UpdateSource } from "@/types/api";

interface DeviceActionsProps {
Expand All @@ -51,9 +57,7 @@ export function DeviceActions({
isRefreshing = false,
}: DeviceActionsProps) {
const { t } = useTranslation();
const [updateChannel, setUpdateChannel] = useState<"stable" | "beta">(
"stable",
);
const [updateChannel, setUpdateChannel] = useState<UpdateChannel>("stable");
const [updateSource, setUpdateSource] = useState<UpdateSource>("internet");
const [rebootDialogOpen, setRebootDialogOpen] = useState(false);
const [updateDialogOpen, setUpdateDialogOpen] = useState(false);
Expand All @@ -64,7 +68,7 @@ export function DeviceActions({
channel,
source,
}: {
channel: "stable" | "beta";
channel: UpdateChannel;
source: UpdateSource;
}) =>
deviceApi.updateDevice(
Expand Down Expand Up @@ -123,8 +127,9 @@ export function DeviceActions({
},
});

const hasUpdates = deviceStatus?.summary.has_updates || false;
const availableUpdates = deviceStatus?.firmware.available_updates || {};
const availableUpdates = deviceStatus?.firmware.available_updates;
const selectedRelease = getChannelRelease(availableUpdates, updateChannel);
const hasUpdates = hasAnyRelease(availableUpdates);

return (
<Card>
Expand Down Expand Up @@ -222,43 +227,62 @@ export function DeviceActions({
</label>
<Select
value={updateChannel}
onValueChange={(value: "stable" | "beta") =>
onValueChange={(value: UpdateChannel) =>
setUpdateChannel(value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stable">
{t("bulkActions.stable")}
</SelectItem>
<SelectItem value="beta">
{t("bulkActions.beta")}
</SelectItem>
{UPDATE_CHANNELS.map((channel) => {
const release = getChannelRelease(
availableUpdates,
channel,
);
const label = t(`bulkActions.${channel}`);
return (
<SelectItem key={channel} value={channel}>
{release
? `${label} (${release.version})`
: label}
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
)}

{updateSource === "internet" && selectedRelease && (
<div className="p-3 bg-muted rounded-lg space-y-2">
<div className="text-sm font-medium">
{selectedRelease.name ||
t("deviceDetail.dialogs.updateFirmware.firmwareUpdate")}
</div>
<div className="text-sm text-muted-foreground">
{t("deviceDetail.dialogs.updateFirmware.version")}:{" "}
{selectedRelease.version}
</div>
{selectedRelease.desc && (
<div className="text-xs text-muted-foreground">
{selectedRelease.desc}
</div>
)}
</div>
)}

{updateSource === "internet" &&
availableUpdates[updateChannel] && (
<div className="p-3 bg-muted rounded-lg space-y-2">
<div className="text-sm font-medium">
{availableUpdates[updateChannel].name ||
t(
"deviceDetail.dialogs.updateFirmware.firmwareUpdate",
deviceStatus &&
!selectedRelease && (
<div className="p-3 bg-muted rounded-lg text-sm text-muted-foreground">
{hasUpdates
? t(
"deviceDetail.dialogs.updateFirmware.noUpdateOnChannel",
)
: t(
"deviceDetail.dialogs.updateFirmware.noUpdatesAvailable",
)}
</div>
<div className="text-sm text-muted-foreground">
{t("deviceDetail.dialogs.updateFirmware.version")}:{" "}
{availableUpdates[updateChannel].version}
</div>
{availableUpdates[updateChannel].desc && (
<div className="text-xs text-muted-foreground">
{availableUpdates[updateChannel].desc}
</div>
)}
</div>
)}
</div>
Expand All @@ -279,7 +303,7 @@ export function DeviceActions({
}
disabled={
updateMutation.isPending ||
(updateSource === "internet" && !hasUpdates)
(updateSource === "internet" && !selectedRelease)
}
>
{updateMutation.isPending
Expand Down
60 changes: 30 additions & 30 deletions packages/web/src/components/device-detail/device-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { getChannelReleases } from "@/lib/firmware-updates";
import type { DeviceStatus } from "@/types/api";

interface DeviceHeaderProps {
Expand Down Expand Up @@ -57,6 +58,9 @@ export function DeviceHeader({ deviceStatus, isLoading }: DeviceHeaderProps) {
}

const { summary, ip } = deviceStatus;
const channelReleases = getChannelReleases(
deviceStatus.firmware.available_updates,
);

const getStatusBadge = () => {
return (
Expand Down Expand Up @@ -133,40 +137,36 @@ export function DeviceHeader({ deviceStatus, isLoading }: DeviceHeaderProps) {
</div>

{/* Available Updates */}
{deviceStatus.firmware.available_updates &&
Object.keys(deviceStatus.firmware.available_updates).length >
0 && (
{channelReleases.length > 0 && (
<div className="space-y-1">
<div className="text-xs text-muted-foreground">
{t("deviceDetail.deviceInfo.availableUpdates")}:
</div>
<div className="space-y-1">
<div className="text-xs text-muted-foreground">
{t("deviceDetail.deviceInfo.availableUpdates")}:
</div>
<div className="space-y-1">
{Object.entries(
deviceStatus.firmware.available_updates,
).map(([channel, update]) => (
<div
key={channel}
className="flex items-center space-x-2"
{channelReleases.map(({ channel, release }) => (
<div
key={channel}
className="flex items-center space-x-2"
>
<Badge
variant="secondary"
className="text-xs px-2 py-0"
>
<Badge
variant="secondary"
className="text-xs px-2 py-0"
>
{channel}
</Badge>
<span className="text-xs font-mono">
{update.version}
{channel}
</Badge>
<span className="text-xs font-mono">
{release.version}
</span>
{release.name && (
<span className="text-xs text-muted-foreground">
({release.name})
</span>
{update.name && (
<span className="text-xs text-muted-foreground">
({update.name})
</span>
)}
</div>
))}
</div>
)}
</div>
))}
</div>
)}
</div>
)}
</div>
</div>

Expand Down
15 changes: 12 additions & 3 deletions packages/web/src/hooks/useComponentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const GET_ACTIONS = [
"ListAPClients",
];

/** Reads that still change what a cached status would say, so they refresh it
* despite being in GET_ACTIONS. */
const STATUS_REFRESHING_ACTIONS = ["CheckForUpdate"];

interface ExecuteComponentActionParams {
deviceIp: string;
componentKey: string;
Expand Down Expand Up @@ -92,9 +96,7 @@ export function useExecuteComponentAction(
parameters,
),
onSuccess: (result, variables) => {
if (
!GET_ACTIONS.some((getAction) => variables.action.includes(getAction))
) {
if (shouldRefreshStatus(variables.action)) {
queryClient.invalidateQueries({
queryKey: queryKeys.devices.status(variables.deviceIp),
});
Expand Down Expand Up @@ -313,6 +315,13 @@ export function shouldShowResponseData(action: string): boolean {
);
}

function shouldRefreshStatus(action: string): boolean {
if (STATUS_REFRESHING_ACTIONS.some((refresh) => action.includes(refresh))) {
return true;
}
return !GET_ACTIONS.some((getAction) => action.includes(getAction));
}

/**
* Checks if a response contains meaningful data to display
*/
Expand Down
4 changes: 3 additions & 1 deletion packages/web/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,9 @@
"version": "Version",
"firmwareUpdate": "Firmware Update",
"startingUpdate": "Starting Update...",
"startUpdate": "Start Update"
"startUpdate": "Start Update",
"noUpdateOnChannel": "No update available on this channel.",
"noUpdatesAvailable": "This device reports no firmware updates available."
},
"rebootDevice": {
"title": "Reboot Device",
Expand Down
37 changes: 37 additions & 0 deletions packages/web/src/lib/firmware-updates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { UpdateInfo } from "@/types/api";

export const UPDATE_CHANNELS = ["stable", "beta"] as const;

export type UpdateChannel = (typeof UPDATE_CHANNELS)[number];

export type FirmwareRelease = UpdateInfo & { version: string };

type AvailableUpdates = Record<string, UpdateInfo | undefined>;

export function getChannelRelease(
availableUpdates: AvailableUpdates | undefined,
channel: string,
): FirmwareRelease | null {
const release = availableUpdates?.[channel];
return isRelease(release) ? release : null;
}

export function getChannelReleases(
availableUpdates: AvailableUpdates | undefined,
): { channel: string; release: FirmwareRelease }[] {
return Object.entries(availableUpdates ?? {}).flatMap(([channel, release]) =>
isRelease(release) ? [{ channel, release }] : [],
);
}

export function hasAnyRelease(
availableUpdates: AvailableUpdates | undefined,
): boolean {
return Object.values(availableUpdates ?? {}).some(isRelease);
}

function isRelease(
release: UpdateInfo | undefined,
): release is FirmwareRelease {
return typeof release?.version === "string" && release.version.length > 0;
}
15 changes: 12 additions & 3 deletions packages/web/src/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,21 @@ export interface EM1DataComponent extends Component {
}

export interface UpdateInfo {
version: string;
build_id: string;
version?: string;
build_id?: string;
name?: string;
desc?: string;
}

/** Summary entries rename desc to description, so this is not interchangeable
* with UpdateInfo. */
export interface SummaryUpdateInfo {
version: string;
build_id: string | null;
name: string | null;
description: string | null;
}

export interface DeviceSummary {
device_name: string | null;
mac_address: string | null;
Expand All @@ -298,7 +307,7 @@ export interface DeviceSummary {
total_power: number;
any_switch_on: boolean;
has_updates: boolean;
available_updates: Record<string, UpdateInfo>;
available_updates: Record<string, SummaryUpdateInfo>;
restart_required: boolean;
last_updated: string;
}
Expand Down
Loading