Skip to content
Closed
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
47 changes: 21 additions & 26 deletions client/src/protoFleet/api/buildings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,20 @@ interface DeleteBuildingProps {
onFinally?: () => void;
}

interface AssignRackToBuildingProps {
// RackPlacementInput is one rack's slot in an AssignRacksToBuilding
// batch. aisleIndex + positionInAisle must be paired (both set or both
// unset); the server rejects half-set inputs.
export interface RackPlacementInput {
rackId: bigint;
// Unset = unassign from any building.
buildingId?: bigint;
// Optional grid cell. Must be paired.
aisleIndex?: number;
positionInAisle?: number;
}

interface AssignRacksToBuildingProps {
// Bulk-friendly. Pass a single-element array for the singular case.
racks: RackPlacementInput[];
// Unset = unassign every rack in the batch from any building.
targetBuildingId?: bigint;
signal?: AbortSignal;
onSuccess?: (siteReassignedDeviceCount: bigint) => void;
onError?: (message: string) => void;
Expand Down Expand Up @@ -353,29 +360,17 @@ const useBuildings = () => {
[handleAuthErrors],
);

// assignRackToBuilding wraps the dedicated rack-positioning RPC.
// Unset `buildingId` unassigns the rack; passing both `aisleIndex`
// and `positionInAisle` positions the rack at that grid cell.
// Passing one without the other is rejected by the server; the
// wrapper preserves the failure surface so callers can react.
const assignRackToBuilding = useCallback(
async ({
rackId,
buildingId,
aisleIndex,
positionInAisle,
signal,
onSuccess,
onError,
onFinally,
}: AssignRackToBuildingProps) => {
// assignRacksToBuilding wraps the bulk rack-positioning RPC.
// Unset `targetBuildingId` unassigns every rack in the batch;
// each rack's `aisleIndex` + `positionInAisle` must be paired.
// The server rejects half-set inputs and out-of-bounds positions.
const assignRacksToBuilding = useCallback(
async ({ racks, targetBuildingId, signal, onSuccess, onError, onFinally }: AssignRacksToBuildingProps) => {
try {
const response = await buildingsClient.assignRackToBuilding(
const response = await buildingsClient.assignRacksToBuilding(
{
rackId,
buildingId,
aisleIndex,
positionInAisle,
racks,
targetBuildingId,
},
{ signal },
);
Expand Down Expand Up @@ -404,7 +399,7 @@ const useBuildings = () => {
createBuilding,
updateBuilding,
deleteBuilding,
assignRackToBuilding,
assignRacksToBuilding,
};
};

Expand Down
102 changes: 63 additions & 39 deletions client/src/protoFleet/api/generated/buildings/v1/buildings_pb.ts

Large diffs are not rendered by default.

187 changes: 23 additions & 164 deletions client/src/protoFleet/api/generated/collection/v1/collection_pb.ts

Large diffs are not rendered by default.

226 changes: 158 additions & 68 deletions client/src/protoFleet/api/generated/device_set/v1/device_set_pb.ts

Large diffs are not rendered by default.

177 changes: 135 additions & 42 deletions client/src/protoFleet/api/generated/sites/v1/sites_pb.ts

Large diffs are not rendered by default.

93 changes: 81 additions & 12 deletions client/src/protoFleet/api/sites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,11 +114,17 @@ interface DeleteSiteProps {
onFinally?: () => void;
}

interface ReassignDevicesToSiteProps {
interface AssignDevicesToSiteProps {
// Unset routes the devices to the "Unassigned" bucket; the create flow
// always supplies a target so this is typically set in practice.
targetSiteId?: bigint;
deviceIdentifiers: string[];
// When true, the server clears any conflicting rack memberships
// inside the same transaction as the site write. Lets cross-site
// reparent skip the client-side remove-from-rack loop and the
// orphan window it created. When false/unset the server returns
// DEVICE_IN_RACK_AT_OTHER_SITE conflicts (today's behavior).
forceClearConflictingRackMembership?: boolean;
signal?: AbortSignal;
onSuccess?: (reassignedCount: bigint) => void;
// conflicts is populated when the server rejects the batch on
Expand All @@ -128,16 +134,35 @@ interface ReassignDevicesToSiteProps {
onFinally?: () => void;
}

interface AssignBuildingToSiteProps {
buildingId: bigint;
// Unset moves the building to "Unassigned".
interface AssignBuildingsToSiteProps {
// Bulk-friendly. Pass a single-element array for the singular case.
buildingIds: bigint[];
// Unset moves the buildings to "Unassigned".
targetSiteId?: bigint;
signal?: AbortSignal;
onSuccess?: (reassignedRackCount: bigint, reassignedDeviceCount: bigint) => void;
onError?: (message: string) => void;
onFinally?: () => void;
}

interface AssignRacksToSiteProps {
// Bulk-friendly. Pass a single-element array for the singular case.
rackIds: bigint[];
// Unset moves the racks to "Unassigned".
targetSiteId?: bigint;
signal?: AbortSignal;
// onSuccess args: device cascade count, count of racks whose
// building was auto-cleared because the move crossed sites.
// TODO(issue-420 follow-up): consumers must surface clearedBuildingCount
// to the operator (toast or modal) — buildings belong to a single site,
// so crossing sites silently clears the rack's building. No UI consumer
// of this RPC exists yet; when one is wired, push a toast on
// clearedBuildingCount > 0 directing the operator to reassign.
onSuccess?: (reassignedDeviceCount: bigint, clearedBuildingCount: bigint) => void;
onError?: (message: string) => void;
onFinally?: () => void;
}

const useSites = () => {
const { handleAuthErrors } = useAuthErrors();

Expand Down Expand Up @@ -267,13 +292,22 @@ const useSites = () => {
[handleAuthErrors],
);

const reassignDevicesToSite = useCallback(
async ({ targetSiteId, deviceIdentifiers, signal, onSuccess, onError, onFinally }: ReassignDevicesToSiteProps) => {
const assignDevicesToSite = useCallback(
async ({
targetSiteId,
deviceIdentifiers,
forceClearConflictingRackMembership,
signal,
onSuccess,
onError,
onFinally,
}: AssignDevicesToSiteProps) => {
try {
const response = await sitesClient.reassignDevicesToSite(
const response = await sitesClient.assignDevicesToSite(
{
targetSiteId,
deviceIdentifiers,
forceClearConflictingRackMembership,
},
{ signal },
);
Expand All @@ -298,12 +332,12 @@ const useSites = () => {
[handleAuthErrors],
);

const assignBuildingToSite = useCallback(
async ({ buildingId, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingToSiteProps) => {
const assignBuildingsToSite = useCallback(
async ({ buildingIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignBuildingsToSiteProps) => {
try {
const response = await sitesClient.assignBuildingToSite(
const response = await sitesClient.assignBuildingsToSite(
{
buildingId,
buildingIds,
targetSiteId,
},
{ signal },
Expand All @@ -325,7 +359,42 @@ const useSites = () => {
[handleAuthErrors],
);

return { listSites, createSite, updateSite, deleteSite, reassignDevicesToSite, assignBuildingToSite };
const assignRacksToSite = useCallback(
async ({ rackIds, targetSiteId, signal, onSuccess, onError, onFinally }: AssignRacksToSiteProps) => {
try {
const response = await sitesClient.assignRacksToSite(
{
rackIds,
targetSiteId,
},
{ signal },
);
if (signal?.aborted) return;
onSuccess?.(response.reassignedDeviceCount, response.clearedBuildingCount);
} catch (err) {
if (signal?.aborted) return;
handleAuthErrors({
error: err,
onError: (error) => {
onError?.(getErrorMessage(error));
},
});
} finally {
onFinally?.();
}
},
[handleAuthErrors],
);

return {
listSites,
createSite,
updateSite,
deleteSite,
assignDevicesToSite,
assignBuildingsToSite,
assignRacksToSite,
};
};

export { useSites };
Loading
Loading