No buildings added to this site
+ {/* Single affordance — the picker itself carries the
+ "New building" hand-off for creating one from scratch. */}
) : sortedEntries.length === 0 ? (
-
+
) : (
sortedEntries.map((b) => (
@@ -390,15 +389,39 @@ const ManageSiteModal = ({
/>
{showManageBuildings ? (
- // siteId 0n in create mode (no persisted site yet): the picker treats
- // every unassigned building as eligible and disables buildings already
- // in another site, which is exactly the create-time rule.
setShowManageBuildings(false)}
onConfirm={handleManageBuildingsConfirm}
+ saving={saving}
+ // "New building" swaps the picker for the create modal, abandoning
+ // any unsaved checkbox changes — the picker's Save is what commits
+ // membership, so leaving without it writes nothing.
+ onCreateNewLaunch={() => {
+ setShowManageBuildings(false);
+ setShowCreateBuilding(true);
+ }}
+ />
+ ) : null}
+
+ {showCreateBuilding ? (
+ // Create a building already attached to this site. The Site dropdown is
+ // locked to `site` (initialSiteId set), matching the site-scoped
+ // building-create entry point elsewhere.
+ {
+ await handleCreateBuildingSave(values);
+ }}
+ onDismiss={() => setShowCreateBuilding(false)}
+ saving={saving}
/>
) : null}
>
diff --git a/client/src/protoFleet/features/sites/components/ManageSiteModal/index.ts b/client/src/protoFleet/features/sites/components/ManageSiteModal/index.ts
index fca68b5374..323aea2679 100644
--- a/client/src/protoFleet/features/sites/components/ManageSiteModal/index.ts
+++ b/client/src/protoFleet/features/sites/components/ManageSiteModal/index.ts
@@ -1,4 +1,3 @@
import ManageSiteModal from "./ManageSiteModal";
-export type { ManageSiteModalMode } from "./ManageSiteModal";
export default ManageSiteModal;
diff --git a/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.test.tsx b/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.test.tsx
index d816827003..1a9704b951 100644
--- a/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.test.tsx
+++ b/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.test.tsx
@@ -22,17 +22,19 @@ const makeModals = (overrides: Partial = {}): SiteModalsApi => ({
openManageEdit: vi.fn(),
requestDeleteCurrent: vi.fn(),
dismiss: vi.fn(),
- cancelAll: vi.fn(),
dismissDeleteConfirm: vi.fn(),
detailsContinueCreate: vi.fn(),
detailsSaveEdit: vi.fn(),
manageEditDetails: vi.fn(),
- manageSave: vi.fn().mockResolvedValue(null),
+ manageCreateBuilding: vi.fn().mockResolvedValue(null),
+ manageAssignBuildings: vi.fn().mockResolvedValue(true),
+ manageRemoveBuilding: vi.fn().mockResolvedValue(true),
deleteConfirm: vi.fn().mockResolvedValue(undefined),
...overrides,
});
-vi.mock("@/protoFleet/api/buildings", () => ({
+vi.mock("@/protoFleet/api/buildings", async (importActual) => ({
+ ...(await importActual()),
useBuildings: () => ({
listBuildingsBySite: vi.fn().mockResolvedValue(undefined),
listAllBuildings: vi.fn(),
@@ -85,19 +87,6 @@ describe("SiteModals", () => {
expect(screen.getByText(/Delete site "North DC"\?/)).toBeInTheDocument();
});
- it("Delete in manageCreateEditingDetails calls cancelAll (no cascade dialog)", () => {
- const modals = makeModals({
- state: { kind: "manageCreateEditingDetails", draft: { ...emptySiteFormValues(), name: "Pending" } },
- });
-
- render();
-
- fireEvent.click(screen.getByTestId("site-settings-modal-delete"));
-
- expect(modals.cancelAll).toHaveBeenCalled();
- expect(modals.requestDeleteCurrent).not.toHaveBeenCalled();
- });
-
it("Cancelling the cascade dialog dismisses only the dialog, leaving the underlying modals", () => {
const site = create(SiteSchema, { id: 42n, name: "North DC" });
const siteWithCounts = create(SiteWithCountsSchema, {
diff --git a/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.tsx b/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.tsx
index c0af2f9358..63c3b9500b 100644
--- a/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.tsx
+++ b/client/src/protoFleet/features/sites/components/SiteModals/SiteModals.tsx
@@ -17,44 +17,33 @@ interface SiteModalsProps {
const SiteModals = ({ modals, sites, buildingsRefreshKey }: SiteModalsProps) => {
const { state, deleteTarget } = modals;
- const showManage =
- state.kind === "manageCreate" ||
- state.kind === "manageEdit" ||
- state.kind === "manageCreateEditingDetails" ||
- state.kind === "manageEditEditingDetails";
+ const showManage = state.kind === "manageEdit" || state.kind === "manageEditEditingDetails";
- // Delete in the create flow (whether or not details is stacked) discards
- // the pending create; edit-flow routes through requestDeleteCurrent which
- // opens the cascade dialog from the page-level cache.
- const handleDelete = () => {
- if (state.kind === "manageCreate" || state.kind === "manageCreateEditingDetails") {
- modals.cancelAll();
- return;
- }
- modals.requestDeleteCurrent(sites);
- };
+ // The manage modal is always backed by a persisted site (Continue creates it
+ // up front), so Delete always opens the cascade dialog from the page-level
+ // cache rather than discarding a pending create.
+ const handleDelete = () => modals.requestDeleteCurrent(sites);
- const manageDraft = showManage ? state.draft : undefined;
- const manageSite = state.kind === "manageEdit" || state.kind === "manageEditEditingDetails" ? state.site : undefined;
- const manageMode = state.kind === "manageEdit" || state.kind === "manageEditEditingDetails" ? "edit" : "create";
+ const manageSite = showManage ? state.site : undefined;
return (
<>
{/* Render ManageSiteModal first so SiteSettingsModal's portal lands
later in the DOM and naturally stacks on top at the same z-50. */}
- {showManage && manageDraft ? (
+ {showManage && manageSite ? (
saving={modals.saving}
/>
) : null}
- {state.kind === "manageCreateEditingDetails" ? (
-
- ) : null}
{state.kind === "manageEditEditingDetails" ? (
{
expect((screen.getByTestId("site-settings-capacity-input") as HTMLInputElement).value).toBe("8");
});
- it("Save calls onSave with the typed values", () => {
- const onSave = vi.fn();
+ it("disables Save until a field actually changes", () => {
render(
undefined}
onDeleteRequested={() => undefined}
onDismiss={() => undefined}
/>,
);
+ const save = screen.getByTestId("site-settings-modal-save");
+ // Save fires UpdateSite; with no edit there's nothing to write.
+ expect(save).toBeDisabled();
+
fireEvent.change(screen.getByTestId("site-settings-name-input"), { target: { value: "East DC 2" } });
- fireEvent.click(screen.getByTestId("site-settings-modal-save"));
+ expect(save).not.toBeDisabled();
- expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ name: "East DC 2", powerCapacityMw: 0 }));
+ // Back to the original value → clean again, not latched dirty.
+ fireEvent.change(screen.getByTestId("site-settings-name-input"), { target: { value: "East DC" } });
+ expect(save).toBeDisabled();
});
- it("Delete triggers onDeleteRequested", () => {
- const onDeleteRequested = vi.fn();
+ it("ignores whitespace-only and capacity re-formatting as edits", () => {
render(
undefined}
- onDeleteRequested={onDeleteRequested}
- onDismiss={() => undefined}
- />,
- );
-
- fireEvent.click(screen.getByTestId("site-settings-modal-delete"));
- expect(onDeleteRequested).toHaveBeenCalled();
- });
-});
-
-describe("SiteSettingsModal — createReturn mode", () => {
- it("renders Delete + Save instead of Cancel + Continue", () => {
- render(
- undefined}
onDeleteRequested={() => undefined}
onDismiss={() => undefined}
/>,
);
- expect(screen.queryByTestId("site-settings-modal-continue")).toBeNull();
- expect(screen.queryByTestId("site-settings-modal-cancel")).toBeNull();
- expect(screen.getByTestId("site-settings-modal-delete")).toBeInTheDocument();
- expect(screen.getByTestId("site-settings-modal-save")).toBeInTheDocument();
+ const save = screen.getByTestId("site-settings-modal-save");
+ // The gate compares the same normalized shape buildValues produces, so
+ // neither of these is a real change.
+ fireEvent.change(screen.getByTestId("site-settings-name-input"), { target: { value: "East DC " } });
+ expect(save).toBeDisabled();
+ fireEvent.change(screen.getByTestId("site-settings-capacity-input"), { target: { value: "8.0" } });
+ expect(save).toBeDisabled();
});
- it("Save invokes onContinue with the current values", () => {
- const onContinue = vi.fn();
+ it("Save calls onSave with the typed values", () => {
+ const onSave = vi.fn();
render(
undefined}
onDismiss={() => undefined}
/>,
);
- fireEvent.change(screen.getByTestId("site-settings-name-input"), { target: { value: "North DC 2" } });
+ fireEvent.change(screen.getByTestId("site-settings-name-input"), { target: { value: "East DC 2" } });
fireEvent.click(screen.getByTestId("site-settings-modal-save"));
- expect(onContinue).toHaveBeenCalledWith(expect.objectContaining({ name: "North DC 2" }));
+ expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ name: "East DC 2", powerCapacityMw: 0 }));
});
- it("Delete invokes onDeleteRequested (discard pending create)", () => {
+ it("Delete triggers onDeleteRequested", () => {
const onDeleteRequested = vi.fn();
render(
undefined}
+ mode="edit"
+ initialValues={baseValues({ name: "East DC" })}
+ onSave={() => undefined}
onDeleteRequested={onDeleteRequested}
onDismiss={() => undefined}
/>,
diff --git a/client/src/protoFleet/features/sites/components/SiteSettingsModal/SiteSettingsModal.tsx b/client/src/protoFleet/features/sites/components/SiteSettingsModal/SiteSettingsModal.tsx
index aa6918e9fa..9d238c9a79 100644
--- a/client/src/protoFleet/features/sites/components/SiteSettingsModal/SiteSettingsModal.tsx
+++ b/client/src/protoFleet/features/sites/components/SiteSettingsModal/SiteSettingsModal.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useState } from "react";
+import { useCallback, useMemo, useState } from "react";
import { type SiteFormValues } from "@/protoFleet/api/sites";
import {
@@ -14,11 +14,10 @@ import Modal from "@/shared/components/Modal";
import Select from "@/shared/components/Select";
import Textarea from "@/shared/components/Textarea";
-// "createReturn" is the state when the operator clicks "Edit details" from
-// ManageSiteModal during the create flow — they're already mid-create, so the
-// CTAs read Delete (discard pending site) + Save (apply changes and return to
-// the manage view) instead of Cancel + Continue.
-export type SiteSettingsModalMode = "create" | "createReturn" | "edit";
+// "create" is the initial site-details step. Continue persists the site
+// (CreateSite) and hands off to ManageSiteModal in edit mode; once the site
+// exists, "Edit details" reopens this modal in "edit" mode (UpdateSite).
+export type SiteSettingsModalMode = "create" | "edit";
interface SiteSettingsModalCommonProps {
open: boolean;
@@ -29,12 +28,7 @@ interface SiteSettingsModalCommonProps {
export type SiteSettingsModalProps = SiteSettingsModalCommonProps &
(
- | { mode: "create"; onContinue: (values: SiteFormValues) => void }
- | {
- mode: "createReturn";
- onContinue: (values: SiteFormValues) => void;
- onDeleteRequested: () => void;
- }
+ | { mode: "create"; onContinue: (values: SiteFormValues) => Promise | void }
| {
mode: "edit";
onSave: (values: SiteFormValues) => Promise | void;
@@ -92,12 +86,33 @@ const SiteSettingsModal = (props: SiteSettingsModalProps) => {
if (props.mode === "edit") {
await props.onSave(values);
} else {
- props.onContinue(values);
+ await props.onContinue(values);
}
}, [buildValues, props]);
const nameValid = name.trim().length > 0;
- const primaryDisabled = !nameValid || saving;
+
+ // Edit-mode dirty gate. Compared against the same normalized shape
+ // buildValues produces, so trailing whitespace or a capacity retyped as
+ // "12.50" doesn't read as an edit. buildValues sets capacityError as a side
+ // effect, so the comparison uses its own parse instead of calling it here.
+ const isDirty = useMemo(() => {
+ const capacity = parseCapacity(capacityText);
+ return (
+ name.trim() !== initialValues.name ||
+ address.trim() !== initialValues.address ||
+ city.trim() !== initialValues.locationCity ||
+ state.trim() !== initialValues.locationState ||
+ postalCode.trim() !== initialValues.postalCode ||
+ (country || "US") !== (initialValues.country || "US") ||
+ timezone !== initialValues.timezone ||
+ capacity !== initialValues.powerCapacityMw ||
+ notes !== initialValues.notes
+ );
+ }, [name, address, city, state, postalCode, country, timezone, capacityText, notes, initialValues]);
+
+ // Create has no baseline to diff against, so it gates on validation only.
+ const primaryDisabled = !nameValid || saving || (props.mode === "edit" && !isDirty);
const buttons =
props.mode === "create"
@@ -106,10 +121,14 @@ const SiteSettingsModal = (props: SiteSettingsModalProps) => {
text: "Cancel",
variant: variants.secondary,
onClick: onDismiss,
+ disabled: saving,
testId: "site-settings-modal-cancel",
},
{
- text: "Continue",
+ // Named for the write it performs (CreateSite) rather than "Continue"
+ // — the site exists once this lands, even if the operator bails out
+ // of the manage step that follows.
+ text: saving ? "Creating…" : "Create site",
variant: variants.primary,
onClick: handlePrimary,
disabled: primaryDisabled,
diff --git a/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts b/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts
index 3c447c3451..3941303b0c 100644
--- a/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts
+++ b/client/src/protoFleet/features/sites/hooks/useSiteModals.test.ts
@@ -5,7 +5,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { create } from "@bufbuild/protobuf";
import { useSiteModals } from "./useSiteModals";
-import { sitesClient } from "@/protoFleet/api/clients";
+import { buildingsClient, sitesClient } from "@/protoFleet/api/clients";
+import { BuildingSchema, CreateBuildingResponseSchema } from "@/protoFleet/api/generated/buildings/v1/buildings_pb";
import {
AssignBuildingsToSiteResponseSchema,
type CreateSiteResponse,
@@ -29,6 +30,9 @@ vi.mock("@/protoFleet/api/clients", () => ({
assignDevicesToSite: vi.fn(),
assignBuildingsToSite: vi.fn(),
},
+ buildingsClient: {
+ createBuilding: vi.fn(),
+ },
}));
vi.mock("@/protoFleet/store", async () => {
@@ -87,49 +91,42 @@ describe("useSiteModals", () => {
});
});
- it("detailsContinueCreate round-trips manageCreate ↔ details preserving edited fields", () => {
- const { result } = renderHook(() => useSiteModals({ refetchSites: vi.fn() }), { wrapper });
+ it("detailsContinueCreate persists the site and opens manageEdit against the new row", async () => {
+ const { create: createResp } = makeSiteResponse(7n, "North DC", "10.0.0.0/24");
+ vi.mocked(sitesClient.createSite).mockResolvedValue(createResp);
+ const refetchSites = vi.fn();
+ const { result } = renderHook(() => useSiteModals({ refetchSites }), { wrapper });
act(() => result.current.openCreate());
- act(() =>
- result.current.detailsContinueCreate({
+
+ await act(async () => {
+ await result.current.detailsContinueCreate({
...emptySiteFormValues(),
name: "North DC",
- locationCity: "Chicago",
- locationState: "IL",
- powerCapacityMw: 5,
- }),
- );
- act(() => result.current.manageEditDetails());
- expect(result.current.state.kind).toBe("manageCreateEditingDetails");
- act(() =>
- result.current.detailsContinueCreate({
- ...emptySiteFormValues(),
- name: "North DC 2",
- locationCity: "Chicago",
- locationState: "IL",
- powerCapacityMw: 5,
- }),
- );
- expect(result.current.state.kind).toBe("manageCreate");
- if (result.current.state.kind === "manageCreate") {
- expect(result.current.state.draft.name).toBe("North DC 2");
- expect(result.current.state.draft.powerCapacityMw).toBe(5);
+ networkConfig: "10.0.0.0/24",
+ });
+ });
+
+ expect(sitesClient.createSite).toHaveBeenCalledTimes(1);
+ expect(refetchSites).toHaveBeenCalled();
+ // The manage modal now runs in edit mode against the freshly-created site.
+ expect(result.current.state.kind).toBe("manageEdit");
+ if (result.current.state.kind === "manageEdit") {
+ expect(result.current.state.site.id).toBe(7n);
+ expect(result.current.state.draft.name).toBe("North DC");
}
});
- it("dismiss from manageCreateEditingDetails returns to manageCreate", () => {
+ it("detailsContinueCreate stays on details when CreateSite fails", async () => {
+ vi.mocked(sitesClient.createSite).mockRejectedValue(new Error("boom"));
const { result } = renderHook(() => useSiteModals({ refetchSites: vi.fn() }), { wrapper });
act(() => result.current.openCreate());
- act(() =>
- result.current.detailsContinueCreate({
- ...emptySiteFormValues(),
- name: "X",
- }),
- );
- act(() => result.current.manageEditDetails());
- expect(result.current.state.kind).toBe("manageCreateEditingDetails");
- act(() => result.current.dismiss());
- expect(result.current.state.kind).toBe("manageCreate");
+
+ await act(async () => {
+ await result.current.detailsContinueCreate({ ...emptySiteFormValues(), name: "North DC" });
+ });
+
+ // No transition — the operator can fix the input and retry.
+ expect(result.current.state.kind).toBe("detailsCreate");
});
it("manageEditDetails on manageEdit stacks to manageEditEditingDetails; dismiss drops back to manageEdit", () => {
@@ -142,63 +139,48 @@ describe("useSiteModals", () => {
expect(result.current.state.kind).toBe("manageEdit");
});
- it("manageSave on manageCreate runs CreateSite and reports closeOnSuccess", async () => {
- const { create: createResp } = makeSiteResponse(7n, "North DC", "10.0.0.0/24");
- vi.mocked(sitesClient.createSite).mockResolvedValue(createResp);
- const refetchSites = vi.fn();
- const { result } = renderHook(() => useSiteModals({ refetchSites }), { wrapper });
- act(() => result.current.openCreate());
- act(() =>
- result.current.detailsContinueCreate({
- ...emptySiteFormValues(),
- name: "North DC",
- networkConfig: "10.0.0.0/24",
+ it("manageCreateBuilding creates a building against the managed site and returns it", async () => {
+ vi.mocked(buildingsClient.createBuilding).mockResolvedValue(
+ create(CreateBuildingResponseSchema, {
+ building: create(BuildingSchema, { id: 42n, name: "Bldg A", siteId: 3n }),
+ assignedRackCount: 0n,
+ reassignedDeviceCount: 0n,
+ conflicts: [],
}),
);
-
- let saveResult: { closeOnSuccess: boolean } | null | undefined;
- await act(async () => {
- saveResult = await result.current.manageSave({ added: [], removed: [] });
- });
-
- await waitFor(() => {
- expect(sitesClient.createSite).toHaveBeenCalledTimes(1);
- });
- // With no staged buildings the create skips the buildings RPC entirely.
- expect(sitesClient.assignBuildingsToSite).not.toHaveBeenCalled();
- expect(saveResult?.closeOnSuccess).toBe(true);
- expect(refetchSites).toHaveBeenCalled();
- });
-
- it("manageSave on manageCreate assigns staged buildings to the new site", async () => {
- const { create: createResp } = makeSiteResponse(7n, "North DC");
- vi.mocked(sitesClient.createSite).mockResolvedValue(createResp);
- vi.mocked(sitesClient.assignBuildingsToSite).mockResolvedValue(
- create(AssignBuildingsToSiteResponseSchema, { reassignedRackCount: 0n, reassignedDeviceCount: 0n }),
- );
const refetchSites = vi.fn();
+ const site = create(SiteSchema, { id: 3n, name: "North DC" });
const { result } = renderHook(() => useSiteModals({ refetchSites }), { wrapper });
- act(() => result.current.openCreate());
- act(() => result.current.detailsContinueCreate({ ...emptySiteFormValues(), name: "North DC" }));
+ act(() => result.current.openManageEdit(site));
- let saveResult: { closeOnSuccess: boolean } | null | undefined;
+ let created: Awaited> | undefined;
await act(async () => {
- saveResult = await result.current.manageSave({ added: [11n, 12n], removed: [] });
+ created = await result.current.manageCreateBuilding({
+ name: "Bldg A",
+ description: "",
+ powerCapacityMw: 0,
+ overheadKw: 0,
+ aisles: 0,
+ racksPerAisle: 0,
+ physicalRackCount: 0,
+ defaultRackRows: 0,
+ defaultRackColumns: 0,
+ defaultRackOrderIndex: 0,
+ });
});
- await waitFor(() => {
- expect(sitesClient.createSite).toHaveBeenCalledTimes(1);
- });
- // Staged buildings are assigned to the freshly-created site (id 7).
- expect(sitesClient.assignBuildingsToSite).toHaveBeenCalledWith(
- { buildingIds: [11n, 12n], targetSiteId: 7n },
+ // Created against the currently-managed site (id 3).
+ expect(buildingsClient.createBuilding).toHaveBeenCalledWith(
+ expect.objectContaining({ siteId: 3n, name: "Bldg A" }),
expect.anything(),
);
- expect(saveResult?.closeOnSuccess).toBe(true);
+ expect(created?.id).toBe(42n);
+ // Site catalog is refreshed for the building count; the modal injects the
+ // row itself, so the building list is deliberately not refetched here.
expect(refetchSites).toHaveBeenCalled();
});
- it("manageSave on manageEdit applies the building delta via AssignBuildingsToSite", async () => {
+ it("manageAssignBuildings applies the picker delta via AssignBuildingsToSite", async () => {
vi.mocked(sitesClient.assignBuildingsToSite).mockResolvedValue(
create(AssignBuildingsToSiteResponseSchema, { reassignedRackCount: 0n, reassignedDeviceCount: 0n }),
);
@@ -208,9 +190,9 @@ describe("useSiteModals", () => {
const { result } = renderHook(() => useSiteModals({ refetchSites, refetchBuildings }), { wrapper });
act(() => result.current.openManageEdit(site));
- let saveResult: { closeOnSuccess: boolean } | null | undefined;
+ let ok: boolean | undefined;
await act(async () => {
- saveResult = await result.current.manageSave({ added: [10n], removed: [20n] });
+ ok = await result.current.manageAssignBuildings({ added: [10n], removed: [20n] });
});
// Two calls: removed → "Unassigned" (no target), added → this site.
@@ -225,23 +207,44 @@ describe("useSiteModals", () => {
{ buildingIds: [10n], targetSiteId: 3n },
expect.anything(),
);
- expect(saveResult?.closeOnSuccess).toBe(true);
+ expect(ok).toBe(true);
expect(refetchSites).toHaveBeenCalled();
// Membership changed building rows, so the building table refresh fires too.
expect(refetchBuildings).toHaveBeenCalled();
});
- it("manageSave on manageEdit with an empty delta closes without an RPC", async () => {
+ it("manageAssignBuildings with an empty delta fires no RPC", async () => {
const { result } = renderHook(() => useSiteModals({ refetchSites: vi.fn() }), { wrapper });
act(() => result.current.openManageEdit(create(SiteSchema, { id: 3n, name: "North DC" })));
- let saveResult: { closeOnSuccess: boolean } | null | undefined;
+ let ok: boolean | undefined;
await act(async () => {
- saveResult = await result.current.manageSave({ added: [], removed: [] });
+ ok = await result.current.manageAssignBuildings({ added: [], removed: [] });
});
expect(sitesClient.assignBuildingsToSite).not.toHaveBeenCalled();
- expect(saveResult?.closeOnSuccess).toBe(true);
+ expect(ok).toBe(true);
+ });
+
+ it("manageRemoveBuilding unassigns the building immediately", async () => {
+ vi.mocked(sitesClient.assignBuildingsToSite).mockResolvedValue(
+ create(AssignBuildingsToSiteResponseSchema, { reassignedRackCount: 0n, reassignedDeviceCount: 0n }),
+ );
+ const refetchSites = vi.fn();
+ const { result } = renderHook(() => useSiteModals({ refetchSites }), { wrapper });
+ act(() => result.current.openManageEdit(create(SiteSchema, { id: 3n, name: "North DC" })));
+
+ let ok: boolean | undefined;
+ await act(async () => {
+ ok = await result.current.manageRemoveBuilding(20n, "Building A");
+ });
+
+ expect(sitesClient.assignBuildingsToSite).toHaveBeenCalledWith(
+ { buildingIds: [20n], targetSiteId: undefined },
+ expect.anything(),
+ );
+ expect(ok).toBe(true);
+ expect(refetchSites).toHaveBeenCalled();
});
it("detailsSaveEdit refreshes manage with server-canonical site on success", async () => {
@@ -368,18 +371,4 @@ describe("useSiteModals", () => {
expect(useFleetStore.getState().ui.activeSite).toEqual({ kind: "site", id: "11", slug: "active" });
});
-
- it("cancelAll closes every modal and clears deleteTarget", () => {
- const site = create(SiteSchema, { id: 5n, name: "T" });
- const { result } = renderHook(() => useSiteModals({ refetchSites: vi.fn() }), { wrapper });
- act(() => result.current.openManageEdit(site));
- act(() =>
- result.current.requestDeleteCurrent([
- create(SiteWithCountsSchema, { site, deviceCount: 0n, rackCount: 0n, buildingCount: 0n }),
- ]),
- );
- act(() => result.current.cancelAll());
- expect(result.current.state.kind).toBe("none");
- expect(result.current.deleteTarget).toBeNull();
- });
});
diff --git a/client/src/protoFleet/features/sites/hooks/useSiteModals.ts b/client/src/protoFleet/features/sites/hooks/useSiteModals.ts
index f820898f02..c95839fb13 100644
--- a/client/src/protoFleet/features/sites/hooks/useSiteModals.ts
+++ b/client/src/protoFleet/features/sites/hooks/useSiteModals.ts
@@ -1,6 +1,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
+import { type BuildingFormValues, useBuildings } from "@/protoFleet/api/buildings";
+import { type Building } from "@/protoFleet/api/generated/buildings/v1/buildings_pb";
import { type Site, type SiteWithCounts } from "@/protoFleet/api/generated/sites/v1/sites_pb";
import { emptySiteFormValues, type SiteFormValues, siteFormValuesFromSite, useSites } from "@/protoFleet/api/sites";
import { scopeCurrentOrDashboardPath, useRouteSiteScope } from "@/protoFleet/routing/siteScope";
@@ -15,15 +17,16 @@ import { pushToast, STATUSES } from "@/shared/features/toaster";
// modal they came from.
export type SiteModalState =
| { kind: "none" }
+ // Initial create step. "Continue" now persists the site (CreateSite) and
+ // transitions straight to manageEdit against the real row, so the manage
+ // surface always has a site id — inline building-create and
+ // AssignBuildingsToSite both need one. Mirrors the seeded "New site" flow in
+ // FleetCreateFlowProvider, which already creates-then-openManageEdit.
| { kind: "detailsCreate"; draft: SiteFormValues }
- | { kind: "manageCreate"; draft: SiteFormValues }
- // Stacked: ManageSiteModal stays open while SiteSettingsModal renders on
- // top. CTAs in details read Delete (discard pending create) + Save (apply
- // changes and return to manage).
- | { kind: "manageCreateEditingDetails"; draft: SiteFormValues }
| { kind: "manageEdit"; site: Site; draft: SiteFormValues }
- // Stacked edit-flow counterpart. Save calls UpdateSite directly; on
- // success details closes and manage stays open with refreshed draft.
+ // Stacked: ManageSiteModal stays open while SiteSettingsModal renders on top.
+ // Save calls UpdateSite directly; on success details closes and manage stays
+ // open with refreshed draft.
| { kind: "manageEditEditingDetails"; site: Site; draft: SiteFormValues };
interface UseSiteModalsOptions {
@@ -56,22 +59,28 @@ export interface SiteModalsApi {
// Closes the topmost modal: drops details if details is stacked on
// manage, otherwise closes everything to none.
dismiss: () => void;
- // Closes every modal regardless of stack — used when the operator
- // discards a pending create from the SiteSettingsModal Delete button.
- cancelAll: () => void;
// SiteDeleteDialog onDismiss — closes only the cascade dialog.
dismissDeleteConfirm: () => void;
- // SiteSettingsModal handlers
- detailsContinueCreate: (values: SiteFormValues) => void;
+ // SiteSettingsModal handlers. Continue persists the site (CreateSite) and,
+ // on success, opens ManageSiteModal in edit mode against the new row.
+ detailsContinueCreate: (values: SiteFormValues) => Promise;
detailsSaveEdit: (values: SiteFormValues) => Promise;
// ManageSiteModal handlers
manageEditDetails: () => void;
- // Persists building-membership changes accumulated in the manage modal.
- // In create mode this runs CreateSite, then assigns any buildings the
- // operator staged (the delta's `added`) to the new site. In edit mode it
- // applies the delta via AssignBuildingsToSite. Returns whether the modal
- // should close on success, or null if the save failed.
- manageSave: (delta: { added: bigint[]; removed: bigint[] }) => Promise<{ closeOnSuccess: boolean } | null>;
+ // Inline building-create from ManageSiteModal. Creates the building against
+ // the currently-managed site via the transactional CreateBuilding RPC and
+ // returns the created row so the modal can inject it into its working set.
+ // Returns null on failure (a toast is shown); a duplicate name surfaces as
+ // a server error and creates nothing.
+ manageCreateBuilding: (values: BuildingFormValues) => Promise;
+ // Commit-per-modal: the buildings picker owns site membership, so its Save
+ // applies the delta via AssignBuildingsToSite right away rather than staging
+ // it into ManageSiteModal. `added` moves buildings into this site; `removed`
+ // moves them to "Unassigned". Resolves true on success.
+ manageAssignBuildings: (delta: { added: bigint[]; removed: bigint[] }) => Promise;
+ // Kebab "Remove building" — an immediate unassign (targetSiteId unset), so
+ // the row action means what it says instead of queueing behind a Save.
+ manageRemoveBuilding: (buildingId: bigint, label: string) => Promise;
// SiteDeleteDialog handlers
deleteConfirm: () => Promise;
}
@@ -101,6 +110,7 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
}, [state]);
const { createSite, updateSite, deleteSite, assignBuildingsToSite } = useSites();
+ const { createBuilding } = useBuildings();
const setActiveSite = useFleetStore((store) => store.ui.setActiveSite);
const activeSite = useFleetStore((store) => store.ui.activeSite);
// Signals the PageHeader's SitePicker (which fetches sites once on mount and
@@ -146,10 +156,9 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
}, []);
const dismiss = useCallback(() => {
- // Stacked states drop just the top (details) and return to the underlying
- // manage state. Everything else closes to none.
+ // The stacked edit-details state drops just the top (details) and returns
+ // to the underlying manage state. Everything else closes to none.
setState((prev) => {
- if (prev.kind === "manageCreateEditingDetails") return { kind: "manageCreate", draft: prev.draft };
if (prev.kind === "manageEditEditingDetails") {
return { kind: "manageEdit", site: prev.site, draft: prev.draft };
}
@@ -157,26 +166,57 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
});
}, []);
- const cancelAll = useCallback(() => {
- setState({ kind: "none" });
- setDeleteTarget(null);
- }, []);
-
const dismissDeleteConfirm = useCallback(() => {
setDeleteTarget(null);
}, []);
- const detailsContinueCreate = useCallback((values: SiteFormValues) => {
- // Carry the existing networkConfig draft through; SiteSettingsModal only
- // owns the descriptive fields, so the value typed in ManageSiteModal
- // survives bouncing between the two surfaces.
- setState((prev) => {
- if (prev.kind === "detailsCreate" || prev.kind === "manageCreateEditingDetails") {
- return { kind: "manageCreate", draft: { ...values, networkConfig: prev.draft.networkConfig } };
+ // Continue on the create-details modal persists the site immediately, then
+ // opens ManageSiteModal in edit mode against the new row. Creating the site
+ // up front (rather than deferring to the manage modal's Save) gives inline
+ // building-create a real site_id to attach to. On failure the details modal
+ // stays open so the operator can fix the input and retry (e.g. a duplicate
+ // name). Guarded so a double-click can't fire two CreateSite calls.
+ const detailsContinueCreate = useCallback(
+ async (values: SiteFormValues) => {
+ if (savingRef.current) return;
+ const current = stateRef.current;
+ if (current.kind !== "detailsCreate") return;
+ // Carry the existing networkConfig draft through; SiteSettingsModal only
+ // owns the descriptive fields, so the value typed elsewhere survives.
+ const draft: SiteFormValues = { ...values, networkConfig: current.draft.networkConfig };
+ savingRef.current = true;
+ setSaving(true);
+ try {
+ await new Promise((resolve) => {
+ void createSite({
+ values: draft,
+ onSuccess: ({ site, networkConfigWarnings }) => {
+ pushToast({
+ message:
+ networkConfigWarnings.length > 0
+ ? `Site "${site.name}" created with warnings`
+ : `Site "${site.name}" created`,
+ status: STATUSES.success,
+ });
+ refetchSites();
+ refetchBuildings?.();
+ bumpSitesRevision();
+ setState({ kind: "manageEdit", site, draft: siteFormValuesFromSite(site) });
+ resolve();
+ },
+ onError: (msg) => {
+ pushToast({ message: `Failed to create site: ${msg}`, status: STATUSES.error });
+ resolve();
+ },
+ });
+ });
+ } finally {
+ savingRef.current = false;
+ setSaving(false);
}
- return prev;
- });
- }, []);
+ },
+ [createSite, refetchSites, refetchBuildings, bumpSitesRevision],
+ );
const detailsSaveEdit = useCallback(
async (values: SiteFormValues) => {
@@ -256,7 +296,6 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
setState((prev) => {
// Stack details on top of manage. Manage stays in the underlying state
// so it remains visible behind SiteSettingsModal.
- if (prev.kind === "manageCreate") return { kind: "manageCreateEditingDetails", draft: prev.draft };
if (prev.kind === "manageEdit") {
return { kind: "manageEditEditingDetails", site: prev.site, draft: prev.draft };
}
@@ -264,135 +303,126 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
});
}, []);
- const manageSave = useCallback(
- async (delta: { added: bigint[]; removed: bigint[] }) => {
+ // Inline building-create from the manage modal. The building is created and
+ // associated to the current site in one transactional RPC (#821), so it
+ // needs no follow-up AssignBuildingsToSite. Returns the created row to the
+ // modal, which injects it into its local working set (rather than triggering
+ // a list refetch) so any buildings staged via the picker are preserved.
+ const manageCreateBuilding = useCallback(
+ async (values: BuildingFormValues): Promise => {
if (savingRef.current) return null;
+ const current = stateRef.current;
+ if (current.kind !== "manageEdit" && current.kind !== "manageEditEditingDetails") return null;
+ const siteId = current.site.id;
+ savingRef.current = true;
+ setSaving(true);
+ return await new Promise((resolve) => {
+ void createBuilding({
+ values,
+ siteId,
+ onSuccess: ({ building }) => {
+ pushToast({ message: `Building "${building.name}" created`, status: STATUSES.success });
+ // Refresh the site catalog (building counts) but deliberately NOT
+ // the modal's building list — the modal injects the new row
+ // locally so unsaved picker selections survive.
+ refetchSites();
+ resolve(building);
+ },
+ onError: (msg) => {
+ pushToast({ message: `Failed to create building: ${msg}`, status: STATUSES.error });
+ resolve(null);
+ },
+ onFinally: () => {
+ savingRef.current = false;
+ setSaving(false);
+ },
+ });
+ });
+ },
+ [createBuilding, refetchSites],
+ );
- // Create flow: persist the site, then assign any buildings the operator
- // staged in the manage modal before the site existed. `added` carries
- // those buildings (the working set started empty, so there's never a
- // `removed`); they're assigned to the freshly-created site's id. The two
- // steps are sequenced explicitly (rather than chained through
- // createSite's onFinally) so the saving guard stays held across both —
- // an async onSuccess would otherwise release it the moment the building
- // assign awaited.
- if (state.kind === "manageCreate") {
- const draft = state.draft;
- savingRef.current = true;
- setSaving(true);
- try {
- const created = await new Promise<{ site: Site; warnings: string[] } | null>((resolve) => {
- void createSite({
- values: draft,
- onSuccess: ({ site, networkConfigWarnings }) => resolve({ site, warnings: networkConfigWarnings }),
- onError: (msg) => {
- pushToast({ message: `Failed to create site: ${msg}`, status: STATUSES.error });
- resolve(null);
- },
- });
- });
- if (!created) return null;
-
- // The site is committed past this point. A subsequent
- // AssignBuildingsToSite failure must not read as a failed create —
- // surface a partial-success warning and still close, matching the
- // bulk "New site" seeded flow in FleetCreateFlowProvider.
- let buildingsFailed: string | null = null;
- if (delta.added.length > 0) {
- await new Promise((resolve) => {
- void assignBuildingsToSite({
- buildingIds: delta.added,
- targetSiteId: created.site.id,
- onSuccess: () => resolve(),
- onError: (msg) => {
- buildingsFailed = msg;
- resolve();
- },
- });
- });
- }
-
- pushToast(
- buildingsFailed
- ? {
- message: `Site "${created.site.name}" created, but adding buildings failed: ${buildingsFailed}`,
- status: STATUSES.error,
- }
- : {
- message:
- created.warnings.length > 0
- ? `Site "${created.site.name}" created with warnings`
- : `Site "${created.site.name}" created`,
- status: STATUSES.success,
- },
- );
- refetchSites();
- refetchBuildings?.();
- bumpSitesRevision();
- return { closeOnSuccess: true };
- } finally {
- savingRef.current = false;
- setSaving(false);
+ // Shared AssignBuildingsToSite dispatcher. `targetSiteId` unset moves the
+ // buildings to "Unassigned"; either way the server cascades site_id down to
+ // racks + devices.
+ const dispatchAssign = useCallback(
+ (buildingIds: bigint[], targetSiteId?: bigint) =>
+ new Promise((resolve, reject) => {
+ if (buildingIds.length === 0) {
+ resolve();
+ return;
}
- }
+ void assignBuildingsToSite({
+ buildingIds,
+ targetSiteId,
+ onSuccess: () => resolve(),
+ onError: (msg) => reject(new Error(msg)),
+ });
+ }),
+ [assignBuildingsToSite],
+ );
- // Edit flow: site details are owned by SiteSettingsModal, so the
- // manage modal's Save only persists building membership. A no-op
- // delta (operator opened Save without changes) closes silently.
- if (state.kind === "manageEdit") {
- if (delta.added.length === 0 && delta.removed.length === 0) {
- return { closeOnSuccess: true };
- }
- const id = state.site.id;
- const name = state.site.name;
- savingRef.current = true;
- setSaving(true);
- try {
- // `added` moves buildings into this site; `removed` moves them to
- // "Unassigned" (targetSiteId unset). Both cascade site_id down to
- // racks + devices server-side. Run sequentially so a mid-chain
- // failure surfaces without a half-applied toast.
- const dispatch = (buildingIds: bigint[], targetSiteId?: bigint) =>
- new Promise((resolve, reject) => {
- if (buildingIds.length === 0) {
- resolve();
- return;
- }
- void assignBuildingsToSite({
- buildingIds,
- targetSiteId,
- onSuccess: () => resolve(),
- onError: (msg) => reject(new Error(msg)),
- });
- });
- try {
- await dispatch(delta.removed, undefined);
- await dispatch(delta.added, id);
- } catch (err) {
- const detail = err instanceof Error ? err.message : "Failed to save buildings";
- pushToast({ message: `Failed to save site: ${detail}`, status: STATUSES.error });
- // The two AssignBuildingsToSite calls aren't atomic across each
- // other: the `removed` batch may have already cascaded buildings
- // out of the site before the `added` batch failed. Refresh so the
- // counts + building table reflect what actually committed rather
- // than the now-stale pre-save view.
- refetchSites();
- refetchBuildings?.();
- return null;
- }
- pushToast({ message: `Site "${name}" saved`, status: STATUSES.success });
- refetchSites();
- refetchBuildings?.();
- return { closeOnSuccess: true };
- } finally {
- savingRef.current = false;
- setSaving(false);
- }
+ const manageAssignBuildings = useCallback(
+ async (delta: { added: bigint[]; removed: bigint[] }): Promise => {
+ if (savingRef.current) return false;
+ const current = stateRef.current;
+ if (current.kind !== "manageEdit" && current.kind !== "manageEditEditingDetails") return false;
+ if (delta.added.length === 0 && delta.removed.length === 0) return true;
+ const id = current.site.id;
+ savingRef.current = true;
+ setSaving(true);
+ try {
+ // Sequential so a mid-chain failure surfaces without a half-applied
+ // success toast.
+ await dispatchAssign(delta.removed, undefined);
+ await dispatchAssign(delta.added, id);
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "Failed to assign buildings";
+ pushToast({ message: `Failed to update buildings: ${detail}`, status: STATUSES.error });
+ // The two calls aren't atomic across each other: the `removed` batch
+ // may have already cascaded buildings out of the site before `added`
+ // failed. Refresh so the counts + building table reflect what actually
+ // committed rather than the now-stale pre-save view.
+ refetchSites();
+ refetchBuildings?.();
+ return false;
+ } finally {
+ savingRef.current = false;
+ setSaving(false);
}
+ const count = delta.added.length + delta.removed.length;
+ pushToast({
+ message: `${count} ${count === 1 ? "building" : "buildings"} updated`,
+ status: STATUSES.success,
+ });
+ refetchSites();
+ refetchBuildings?.();
+ return true;
+ },
+ [dispatchAssign, refetchSites, refetchBuildings],
+ );
- return null;
+ const manageRemoveBuilding = useCallback(
+ async (buildingId: bigint, label: string): Promise => {
+ if (savingRef.current) return false;
+ savingRef.current = true;
+ setSaving(true);
+ try {
+ await dispatchAssign([buildingId], undefined);
+ } catch (err) {
+ const detail = err instanceof Error ? err.message : "Failed to remove building";
+ pushToast({ message: `Failed to remove "${label}": ${detail}`, status: STATUSES.error });
+ return false;
+ } finally {
+ savingRef.current = false;
+ setSaving(false);
+ }
+ pushToast({ message: `"${label}" removed from this site`, status: STATUSES.success });
+ refetchSites();
+ refetchBuildings?.();
+ return true;
},
- [state, createSite, assignBuildingsToSite, refetchSites, refetchBuildings, bumpSitesRevision],
+ [dispatchAssign, refetchSites, refetchBuildings],
);
const deleteConfirm = useCallback(async () => {
@@ -442,12 +472,13 @@ const useSiteModals = ({ refetchSites, refetchBuildings }: UseSiteModalsOptions)
openManageEdit,
requestDeleteCurrent,
dismiss,
- cancelAll,
dismissDeleteConfirm,
detailsContinueCreate,
detailsSaveEdit,
manageEditDetails,
- manageSave,
+ manageCreateBuilding,
+ manageAssignBuildings,
+ manageRemoveBuilding,
deleteConfirm,
};
};
diff --git a/proto/device_set/v1/device_set.proto b/proto/device_set/v1/device_set.proto
index 5961da405b..fb4eeea389 100644
--- a/proto/device_set/v1/device_set.proto
+++ b/proto/device_set/v1/device_set.proto
@@ -90,6 +90,15 @@ service DeviceSetService {
// device_selector accepts only the device_list variant. all_devices
// is rejected with InvalidArgument because moving every paired
// device into a single rack is never the intended operation.
+ //
+ // slot_assignments optionally carries each moved device's slot inside
+ // the target rack, so membership and placement land in the same
+ // transaction. This is the rack-level counterpart to
+ // buildings.v1.AssignRacksToBuilding: a delta that names only the
+ // children it changes, with placement optional per child. Prefer it
+ // over SaveRack for every edit — SaveRack replaces the rack's whole
+ // member set, so a stale client snapshot silently drops members
+ // added concurrently.
rpc AssignDevicesToRack(AssignDevicesToRackRequest) returns (AssignDevicesToRackResponse);
}
@@ -729,6 +738,29 @@ message AssignDevicesToRackRequest {
// `conflicts` and writes nothing. Set true to proceed and clear the
// conflicting miners' site (and building) as part of the move.
optional bool force_clear_conflicting_site = 3;
+
+ // Slot placements for the devices being assigned. The rack-level
+ // analogue of buildings.v1.RackPlacement's optional aisle_index /
+ // position_in_aisle: the batch names only the devices it is changing,
+ // and each named device ends up either placed or explicitly unplaced.
+ //
+ // Per entry: `position` set places the device there, `position` unset
+ // CLEARS its slot. A device not named here is untouched, so one call
+ // can mix placed, unplaced and left-alone miners. Every named device
+ // is cleared before any position is written, which is what lets a
+ // relayout swap two occupied cells without tripping
+ // uk_rack_slot_position mid-batch.
+ //
+ // Empty (the default): no slot is written at all. A device already in
+ // the target rack keeps its slot; a device arriving from another rack
+ // starts unplaced, because its old slot died with its old membership.
+ //
+ // Every entry must name a device present in device_selector — a slot
+ // needs a membership row to hang off — and no two entries may share a
+ // device or a position. Positions are bounds checked against the
+ // target rack's rows/columns. Requires target_rack_id: an unassign has
+ // no rack to place into.
+ repeated RackSlot slot_assignments = 4 [(buf.validate.field).repeated.max_items = 10000];
}
message AssignDevicesToRackResponse {
diff --git a/server/generated/grpc/device_set/v1/device_set.pb.go b/server/generated/grpc/device_set/v1/device_set.pb.go
index 6b9c85b0f5..dad3beb3bb 100644
--- a/server/generated/grpc/device_set/v1/device_set.pb.go
+++ b/server/generated/grpc/device_set/v1/device_set.pb.go
@@ -3251,8 +3251,30 @@ type AssignDevicesToRackRequest struct {
// `conflicts` and writes nothing. Set true to proceed and clear the
// conflicting miners' site (and building) as part of the move.
ForceClearConflictingSite *bool `protobuf:"varint,3,opt,name=force_clear_conflicting_site,json=forceClearConflictingSite,proto3,oneof" json:"force_clear_conflicting_site,omitempty"`
- unknownFields protoimpl.UnknownFields
- sizeCache protoimpl.SizeCache
+ // Slot placements for the devices being assigned. The rack-level
+ // analogue of buildings.v1.RackPlacement's optional aisle_index /
+ // position_in_aisle: the batch names only the devices it is changing,
+ // and each named device ends up either placed or explicitly unplaced.
+ //
+ // Per entry: `position` set places the device there, `position` unset
+ // CLEARS its slot. A device not named here is untouched, so one call
+ // can mix placed, unplaced and left-alone miners. Every named device
+ // is cleared before any position is written, which is what lets a
+ // relayout swap two occupied cells without tripping
+ // uk_rack_slot_position mid-batch.
+ //
+ // Empty (the default): no slot is written at all. A device already in
+ // the target rack keeps its slot; a device arriving from another rack
+ // starts unplaced, because its old slot died with its old membership.
+ //
+ // Every entry must name a device present in device_selector — a slot
+ // needs a membership row to hang off — and no two entries may share a
+ // device or a position. Positions are bounds checked against the
+ // target rack's rows/columns. Requires target_rack_id: an unassign has
+ // no rack to place into.
+ SlotAssignments []*RackSlot `protobuf:"bytes,4,rep,name=slot_assignments,json=slotAssignments,proto3" json:"slot_assignments,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
}
func (x *AssignDevicesToRackRequest) Reset() {
@@ -3306,6 +3328,13 @@ func (x *AssignDevicesToRackRequest) GetForceClearConflictingSite() bool {
return false
}
+func (x *AssignDevicesToRackRequest) GetSlotAssignments() []*RackSlot {
+ if x != nil {
+ return x.SlotAssignments
+ }
+ return nil
+}
+
type AssignDevicesToRackResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Number of devices whose rack membership now points at
@@ -3912,7 +3941,7 @@ var file_device_set_v1_device_set_proto_rawDesc = string([]byte{
0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f,
0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x09, 0x63, 0x6f,
- 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x22, 0x96, 0x02, 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69,
+ 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x22, 0xe5, 0x02, 0x0a, 0x1a, 0x41, 0x73, 0x73, 0x69,
0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74,
0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42, 0x07,
@@ -3926,209 +3955,214 @@ var file_device_set_v1_device_set_proto_rawDesc = string([]byte{
0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e,
0x67, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x19,
0x66, 0x6f, 0x72, 0x63, 0x65, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69,
- 0x63, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x11, 0x0a, 0x0f,
- 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x42,
- 0x1f, 0x0a, 0x1d, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f,
- 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x69, 0x74, 0x65,
- 0x22, 0xe1, 0x01, 0x0a, 0x1b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63,
- 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75,
- 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e,
- 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f,
- 0x72, 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73,
- 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72,
- 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x03, 0x52, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74,
- 0x12, 0x42, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x61, 0x63,
- 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c,
- 0x69, 0x63, 0x74, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x15, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69,
- 0x63, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x2b,
- 0x0a, 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66,
- 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63,
- 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x06, 0x72,
- 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x64, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63,
- 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a,
- 0x65, 0x0a, 0x0d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65,
- 0x12, 0x1f, 0x0a, 0x1b, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54,
- 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10,
- 0x00, 0x12, 0x19, 0x0a, 0x15, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f,
- 0x54, 0x59, 0x50, 0x45, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14,
- 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
- 0x52, 0x41, 0x43, 0x4b, 0x10, 0x02, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f,
- 0x72, 0x64, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43,
- 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e,
- 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52,
- 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f,
- 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a,
- 0x19, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45,
- 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d,
+ 0x63, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x69, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x10,
+ 0x73, 0x6c, 0x6f, 0x74, 0x5f, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73,
+ 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f,
+ 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x42,
+ 0x09, 0xba, 0x48, 0x06, 0x92, 0x01, 0x03, 0x10, 0x90, 0x4e, 0x52, 0x0f, 0x73, 0x6c, 0x6f, 0x74,
+ 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x42, 0x11, 0x0a, 0x0f, 0x5f,
+ 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x64, 0x42, 0x1f,
+ 0x0a, 0x1d, 0x5f, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x63,
+ 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x69, 0x74, 0x65, 0x22,
+ 0xe1, 0x01, 0x0a, 0x1b, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
+ 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x25, 0x0a, 0x0e, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65,
+ 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x69, 0x74, 0x65, 0x5f, 0x72,
+ 0x65, 0x61, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x73, 0x69, 0x74, 0x65, 0x52, 0x65, 0x61, 0x73, 0x73,
+ 0x69, 0x67, 0x6e, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65,
+ 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12,
+ 0x42, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x61, 0x63, 0x6b,
+ 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x66, 0x6c, 0x69,
+ 0x63, 0x74, 0x73, 0x22, 0x88, 0x01, 0x0a, 0x15, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63,
+ 0x65, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x12, 0x2b, 0x0a,
+ 0x11, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69,
+ 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
+ 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x06, 0x72, 0x65,
+ 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 0x2e, 0x64, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x65, 0x72, 0x44, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74,
+ 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x65,
+ 0x0a, 0x0d, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
+ 0x1f, 0x0a, 0x1b, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59,
+ 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00,
+ 0x12, 0x19, 0x0a, 0x15, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54,
+ 0x59, 0x50, 0x45, 0x5f, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x44,
+ 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52,
+ 0x41, 0x43, 0x4b, 0x10, 0x02, 0x2a, 0xb6, 0x01, 0x0a, 0x0e, 0x52, 0x61, 0x63, 0x6b, 0x4f, 0x72,
+ 0x64, 0x65, 0x72, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41, 0x43, 0x4b,
+ 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x55, 0x4e, 0x53,
+ 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x41,
+ 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f, 0x42,
+ 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19,
0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58,
- 0x5f, 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12,
- 0x1e, 0x0a, 0x1a, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e,
- 0x44, 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a,
- 0x70, 0x0a, 0x0f, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79,
- 0x70, 0x65, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49,
- 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46,
- 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f,
- 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01,
- 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47,
- 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4d, 0x4d, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10,
- 0x02, 0x2a, 0xdd, 0x01, 0x0a, 0x10, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
- 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44,
- 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53,
- 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c,
- 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53,
- 0x5f, 0x45, 0x4d, 0x50, 0x54, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54,
- 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48,
- 0x45, 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54,
- 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e,
- 0x45, 0x45, 0x44, 0x53, 0x5f, 0x41, 0x54, 0x54, 0x45, 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03,
- 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f,
- 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04,
- 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f,
- 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4c, 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10,
- 0x05, 0x2a, 0x85, 0x01, 0x0a, 0x1b, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52,
- 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f,
- 0x6e, 0x12, 0x2f, 0x0a, 0x2b, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f,
+ 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x4c, 0x45, 0x46, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52,
+ 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x5f,
+ 0x42, 0x4f, 0x54, 0x54, 0x4f, 0x4d, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x03, 0x12, 0x1e,
+ 0x0a, 0x1a, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x4f, 0x52, 0x44, 0x45, 0x52, 0x5f, 0x49, 0x4e, 0x44,
+ 0x45, 0x58, 0x5f, 0x54, 0x4f, 0x50, 0x5f, 0x52, 0x49, 0x47, 0x48, 0x54, 0x10, 0x04, 0x2a, 0x70,
+ 0x0a, 0x0f, 0x52, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6f, 0x6c, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70,
+ 0x65, 0x12, 0x21, 0x0a, 0x1d, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e,
+ 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49,
+ 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f,
+ 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x49, 0x52, 0x10, 0x01, 0x12,
+ 0x1f, 0x0a, 0x1b, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4f, 0x4c, 0x49, 0x4e, 0x47, 0x5f,
+ 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4d, 0x4d, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02,
+ 0x2a, 0xdd, 0x01, 0x0a, 0x10, 0x53, 0x6c, 0x6f, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53,
+ 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45,
+ 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50,
+ 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x53, 0x4c, 0x4f,
+ 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f,
+ 0x45, 0x4d, 0x50, 0x54, 0x59, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f,
+ 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x48, 0x45,
+ 0x41, 0x4c, 0x54, 0x48, 0x59, 0x10, 0x02, 0x12, 0x26, 0x0a, 0x22, 0x53, 0x4c, 0x4f, 0x54, 0x5f,
+ 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4e, 0x45,
+ 0x45, 0x44, 0x53, 0x5f, 0x41, 0x54, 0x54, 0x45, 0x4e, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12,
+ 0x1e, 0x0a, 0x1a, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53,
+ 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4f, 0x46, 0x46, 0x4c, 0x49, 0x4e, 0x45, 0x10, 0x04, 0x12,
+ 0x1f, 0x0a, 0x1b, 0x53, 0x4c, 0x4f, 0x54, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x53,
+ 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4c, 0x45, 0x45, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05,
+ 0x2a, 0x85, 0x01, 0x0a, 0x1b, 0x50, 0x65, 0x72, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x52, 0x61,
+ 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x6c, 0x69, 0x63, 0x74, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e,
+ 0x12, 0x2f, 0x0a, 0x2b, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x52,
+ 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, 0x5f, 0x52, 0x45, 0x41,
+ 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10,
+ 0x00, 0x12, 0x35, 0x0a, 0x31, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f,
0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, 0x5f, 0x52, 0x45,
- 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44,
- 0x10, 0x00, 0x12, 0x35, 0x0a, 0x31, 0x50, 0x45, 0x52, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45,
- 0x5f, 0x52, 0x41, 0x43, 0x4b, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x4c, 0x49, 0x43, 0x54, 0x5f, 0x52,
- 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4c, 0x4f, 0x53,
- 0x45, 0x53, 0x5f, 0x53, 0x49, 0x54, 0x45, 0x10, 0x01, 0x32, 0xa9, 0x0e, 0x0a, 0x10, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x60,
- 0x0a, 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65,
- 0x74, 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76,
- 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65,
- 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63,
- 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
- 0x12, 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65,
- 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65,
- 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x55, 0x70, 0x64,
- 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, 0x2e, 0x64,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64,
- 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
- 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x44,
- 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25,
- 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44,
- 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
- 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69,
- 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a,
- 0x0e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12,
- 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e,
- 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
- 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65,
- 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11,
- 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75,
- 0x70, 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76,
- 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72,
- 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x65, 0x76,
- 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x2c,
- 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52,
- 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d,
- 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x64,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d,
- 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72,
- 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x4c,
+ 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x4c, 0x4f, 0x53, 0x45,
+ 0x53, 0x5f, 0x53, 0x49, 0x54, 0x45, 0x10, 0x01, 0x32, 0xa9, 0x0e, 0x0a, 0x10, 0x44, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x60, 0x0a,
+ 0x0f, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
+ 0x12, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65,
+ 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x57, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12,
+ 0x22, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e,
+ 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
+ 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x55, 0x70, 0x64, 0x61,
+ 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, 0x2e, 0x64, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61,
+ 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53,
+ 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x0f, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x12, 0x25, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65,
+ 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63,
+ 0x65, 0x53, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5d, 0x0a, 0x0e,
+ 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x24,
+ 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
+ 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53,
+ 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x41,
+ 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70,
+ 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f,
+ 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x64, 0x44, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x16, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x2c, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65,
+ 0x6d, 0x6f, 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47,
+ 0x72, 0x6f, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x64, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6d, 0x6f,
+ 0x76, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x46, 0x72, 0x6f, 0x6d, 0x47, 0x72, 0x6f,
+ 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x14, 0x4c, 0x69,
+ 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65,
+ 0x72, 0x73, 0x12, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
+ 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b,
+ 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d, 0x62,
- 0x65, 0x72, 0x73, 0x12, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65,
- 0x74, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
- 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e,
- 0x4c, 0x69, 0x73, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x4d, 0x65, 0x6d,
- 0x62, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13,
- 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53,
- 0x65, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76,
- 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a,
- 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47,
+ 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x47,
0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65,
- 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x53, 0x65,
- 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
- 0x6e, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76,
- 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73,
- 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74,
+ 0x74, 0x73, 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x53, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65,
+ 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
+ 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x53, 0x65, 0x74,
0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, 0x6c, 0x65, 0x61,
- 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
- 0x6e, 0x12, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76,
- 0x31, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50,
- 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c,
- 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43,
- 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69,
- 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c,
- 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x64,
- 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74,
- 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
- 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69,
- 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76,
- 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74,
- 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a,
- 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x23,
+ 0x12, 0x29, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x53, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69,
+ 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x74, 0x52,
+ 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x15, 0x43, 0x6c, 0x65, 0x61, 0x72,
+ 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
+ 0x12, 0x2b, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31,
+ 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f,
+ 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c,
+ 0x65, 0x61, 0x72, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x50, 0x6f, 0x73, 0x69, 0x74,
+ 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x47,
+ 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x12, 0x22, 0x2e, 0x64, 0x65,
+ 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x52,
+ 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e,
+ 0x47, 0x65, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x53, 0x6c, 0x6f, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63,
+ 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x12, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x53,
+ 0x74, 0x61, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x0d,
+ 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x12, 0x23, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69,
+ 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e,
+ 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74,
+ 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x12, 0x26, 0x2e, 0x64,
+ 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73,
+ 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65,
+ 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e,
+ 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a,
+ 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x23,
0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
- 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
+ 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74,
- 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65,
- 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x10, 0x4c, 0x69, 0x73,
- 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x12, 0x26, 0x2e,
- 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69,
- 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f, 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
- 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x5a, 0x6f,
- 0x6e, 0x65, 0x52, 0x65, 0x66, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5a,
- 0x0a, 0x0d, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12,
- 0x23, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e,
- 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65,
- 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70,
- 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x61,
- 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f,
- 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f,
- 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x69, 0x67,
- 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x12, 0x29,
- 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41,
- 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61,
- 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69,
- 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e,
- 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xc3, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x64, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x44, 0x65, 0x76,
- 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4d, 0x67,
- 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f,
- 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76,
- 0x65, 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70,
- 0x63, 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2f, 0x76, 0x31, 0x3b,
- 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x44,
- 0x58, 0x58, 0xaa, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x2e, 0x56,
- 0x31, 0xca, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31,
- 0xe2, 0x02, 0x18, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31, 0x5c,
- 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x44, 0x65,
- 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f,
- 0x74, 0x6f, 0x33,
+ 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x61, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65,
+ 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x61, 0x76,
+ 0x65, 0x52, 0x61, 0x63, 0x6b, 0x12, 0x1e, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
+ 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73,
+ 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x76, 0x65, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x13, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e,
+ 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x12, 0x29, 0x2e,
+ 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73,
+ 0x73, 0x69, 0x67, 0x6e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63,
+ 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x64, 0x65, 0x76, 0x69, 0x63,
+ 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x44,
+ 0x65, 0x76, 0x69, 0x63, 0x65, 0x73, 0x54, 0x6f, 0x52, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xc3, 0x01, 0x0a, 0x11, 0x63, 0x6f, 0x6d, 0x2e, 0x64, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x44, 0x65, 0x76, 0x69,
+ 0x63, 0x65, 0x53, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4d, 0x67, 0x69,
+ 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x66, 0x6c, 0x65, 0x65, 0x74, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65,
+ 0x72, 0x2f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x67, 0x72, 0x70, 0x63,
+ 0x2f, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x64,
+ 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x44, 0x58,
+ 0x58, 0xaa, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x2e, 0x56, 0x31,
+ 0xca, 0x02, 0x0c, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31, 0xe2,
+ 0x02, 0x18, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47,
+ 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0d, 0x44, 0x65, 0x76,
+ 0x69, 0x63, 0x65, 0x53, 0x65, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x33,
})
var (
@@ -4255,49 +4289,50 @@ var file_device_set_v1_device_set_proto_depIdxs = []int32{
5, // 44: device_set.v1.SaveRackResponse.device_set:type_name -> device_set.v1.DeviceSet
51, // 45: device_set.v1.SaveRackResponse.conflicts:type_name -> device_set.v1.PerDeviceRackConflict
54, // 46: device_set.v1.AssignDevicesToRackRequest.device_selector:type_name -> common.v1.DeviceSelector
- 51, // 47: device_set.v1.AssignDevicesToRackResponse.conflicts:type_name -> device_set.v1.PerDeviceRackConflict
- 4, // 48: device_set.v1.PerDeviceRackConflict.reason:type_name -> device_set.v1.PerDeviceRackConflictReason
- 11, // 49: device_set.v1.DeviceSetService.CreateDeviceSet:input_type -> device_set.v1.CreateDeviceSetRequest
- 13, // 50: device_set.v1.DeviceSetService.GetDeviceSet:input_type -> device_set.v1.GetDeviceSetRequest
- 15, // 51: device_set.v1.DeviceSetService.UpdateDeviceSet:input_type -> device_set.v1.UpdateDeviceSetRequest
- 17, // 52: device_set.v1.DeviceSetService.DeleteDeviceSet:input_type -> device_set.v1.DeleteDeviceSetRequest
- 19, // 53: device_set.v1.DeviceSetService.ListDeviceSets:input_type -> device_set.v1.ListDeviceSetsRequest
- 21, // 54: device_set.v1.DeviceSetService.AddDevicesToGroup:input_type -> device_set.v1.AddDevicesToGroupRequest
- 23, // 55: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:input_type -> device_set.v1.RemoveDevicesFromGroupRequest
- 25, // 56: device_set.v1.DeviceSetService.ListDeviceSetMembers:input_type -> device_set.v1.ListDeviceSetMembersRequest
- 27, // 57: device_set.v1.DeviceSetService.GetDeviceDeviceSets:input_type -> device_set.v1.GetDeviceDeviceSetsRequest
- 29, // 58: device_set.v1.DeviceSetService.SetRackSlotPosition:input_type -> device_set.v1.SetRackSlotPositionRequest
- 31, // 59: device_set.v1.DeviceSetService.ClearRackSlotPosition:input_type -> device_set.v1.ClearRackSlotPositionRequest
- 33, // 60: device_set.v1.DeviceSetService.GetRackSlots:input_type -> device_set.v1.GetRackSlotsRequest
- 37, // 61: device_set.v1.DeviceSetService.GetDeviceSetStats:input_type -> device_set.v1.GetDeviceSetStatsRequest
- 40, // 62: device_set.v1.DeviceSetService.ListRackZones:input_type -> device_set.v1.ListRackZonesRequest
- 42, // 63: device_set.v1.DeviceSetService.ListRackZoneRefs:input_type -> device_set.v1.ListRackZoneRefsRequest
- 44, // 64: device_set.v1.DeviceSetService.ListRackTypes:input_type -> device_set.v1.ListRackTypesRequest
- 47, // 65: device_set.v1.DeviceSetService.SaveRack:input_type -> device_set.v1.SaveRackRequest
- 49, // 66: device_set.v1.DeviceSetService.AssignDevicesToRack:input_type -> device_set.v1.AssignDevicesToRackRequest
- 12, // 67: device_set.v1.DeviceSetService.CreateDeviceSet:output_type -> device_set.v1.CreateDeviceSetResponse
- 14, // 68: device_set.v1.DeviceSetService.GetDeviceSet:output_type -> device_set.v1.GetDeviceSetResponse
- 16, // 69: device_set.v1.DeviceSetService.UpdateDeviceSet:output_type -> device_set.v1.UpdateDeviceSetResponse
- 18, // 70: device_set.v1.DeviceSetService.DeleteDeviceSet:output_type -> device_set.v1.DeleteDeviceSetResponse
- 20, // 71: device_set.v1.DeviceSetService.ListDeviceSets:output_type -> device_set.v1.ListDeviceSetsResponse
- 22, // 72: device_set.v1.DeviceSetService.AddDevicesToGroup:output_type -> device_set.v1.AddDevicesToGroupResponse
- 24, // 73: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:output_type -> device_set.v1.RemoveDevicesFromGroupResponse
- 26, // 74: device_set.v1.DeviceSetService.ListDeviceSetMembers:output_type -> device_set.v1.ListDeviceSetMembersResponse
- 28, // 75: device_set.v1.DeviceSetService.GetDeviceDeviceSets:output_type -> device_set.v1.GetDeviceDeviceSetsResponse
- 30, // 76: device_set.v1.DeviceSetService.SetRackSlotPosition:output_type -> device_set.v1.SetRackSlotPositionResponse
- 32, // 77: device_set.v1.DeviceSetService.ClearRackSlotPosition:output_type -> device_set.v1.ClearRackSlotPositionResponse
- 35, // 78: device_set.v1.DeviceSetService.GetRackSlots:output_type -> device_set.v1.GetRackSlotsResponse
- 38, // 79: device_set.v1.DeviceSetService.GetDeviceSetStats:output_type -> device_set.v1.GetDeviceSetStatsResponse
- 41, // 80: device_set.v1.DeviceSetService.ListRackZones:output_type -> device_set.v1.ListRackZonesResponse
- 43, // 81: device_set.v1.DeviceSetService.ListRackZoneRefs:output_type -> device_set.v1.ListRackZoneRefsResponse
- 46, // 82: device_set.v1.DeviceSetService.ListRackTypes:output_type -> device_set.v1.ListRackTypesResponse
- 48, // 83: device_set.v1.DeviceSetService.SaveRack:output_type -> device_set.v1.SaveRackResponse
- 50, // 84: device_set.v1.DeviceSetService.AssignDevicesToRack:output_type -> device_set.v1.AssignDevicesToRackResponse
- 67, // [67:85] is the sub-list for method output_type
- 49, // [49:67] is the sub-list for method input_type
- 49, // [49:49] is the sub-list for extension type_name
- 49, // [49:49] is the sub-list for extension extendee
- 0, // [0:49] is the sub-list for field type_name
+ 34, // 47: device_set.v1.AssignDevicesToRackRequest.slot_assignments:type_name -> device_set.v1.RackSlot
+ 51, // 48: device_set.v1.AssignDevicesToRackResponse.conflicts:type_name -> device_set.v1.PerDeviceRackConflict
+ 4, // 49: device_set.v1.PerDeviceRackConflict.reason:type_name -> device_set.v1.PerDeviceRackConflictReason
+ 11, // 50: device_set.v1.DeviceSetService.CreateDeviceSet:input_type -> device_set.v1.CreateDeviceSetRequest
+ 13, // 51: device_set.v1.DeviceSetService.GetDeviceSet:input_type -> device_set.v1.GetDeviceSetRequest
+ 15, // 52: device_set.v1.DeviceSetService.UpdateDeviceSet:input_type -> device_set.v1.UpdateDeviceSetRequest
+ 17, // 53: device_set.v1.DeviceSetService.DeleteDeviceSet:input_type -> device_set.v1.DeleteDeviceSetRequest
+ 19, // 54: device_set.v1.DeviceSetService.ListDeviceSets:input_type -> device_set.v1.ListDeviceSetsRequest
+ 21, // 55: device_set.v1.DeviceSetService.AddDevicesToGroup:input_type -> device_set.v1.AddDevicesToGroupRequest
+ 23, // 56: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:input_type -> device_set.v1.RemoveDevicesFromGroupRequest
+ 25, // 57: device_set.v1.DeviceSetService.ListDeviceSetMembers:input_type -> device_set.v1.ListDeviceSetMembersRequest
+ 27, // 58: device_set.v1.DeviceSetService.GetDeviceDeviceSets:input_type -> device_set.v1.GetDeviceDeviceSetsRequest
+ 29, // 59: device_set.v1.DeviceSetService.SetRackSlotPosition:input_type -> device_set.v1.SetRackSlotPositionRequest
+ 31, // 60: device_set.v1.DeviceSetService.ClearRackSlotPosition:input_type -> device_set.v1.ClearRackSlotPositionRequest
+ 33, // 61: device_set.v1.DeviceSetService.GetRackSlots:input_type -> device_set.v1.GetRackSlotsRequest
+ 37, // 62: device_set.v1.DeviceSetService.GetDeviceSetStats:input_type -> device_set.v1.GetDeviceSetStatsRequest
+ 40, // 63: device_set.v1.DeviceSetService.ListRackZones:input_type -> device_set.v1.ListRackZonesRequest
+ 42, // 64: device_set.v1.DeviceSetService.ListRackZoneRefs:input_type -> device_set.v1.ListRackZoneRefsRequest
+ 44, // 65: device_set.v1.DeviceSetService.ListRackTypes:input_type -> device_set.v1.ListRackTypesRequest
+ 47, // 66: device_set.v1.DeviceSetService.SaveRack:input_type -> device_set.v1.SaveRackRequest
+ 49, // 67: device_set.v1.DeviceSetService.AssignDevicesToRack:input_type -> device_set.v1.AssignDevicesToRackRequest
+ 12, // 68: device_set.v1.DeviceSetService.CreateDeviceSet:output_type -> device_set.v1.CreateDeviceSetResponse
+ 14, // 69: device_set.v1.DeviceSetService.GetDeviceSet:output_type -> device_set.v1.GetDeviceSetResponse
+ 16, // 70: device_set.v1.DeviceSetService.UpdateDeviceSet:output_type -> device_set.v1.UpdateDeviceSetResponse
+ 18, // 71: device_set.v1.DeviceSetService.DeleteDeviceSet:output_type -> device_set.v1.DeleteDeviceSetResponse
+ 20, // 72: device_set.v1.DeviceSetService.ListDeviceSets:output_type -> device_set.v1.ListDeviceSetsResponse
+ 22, // 73: device_set.v1.DeviceSetService.AddDevicesToGroup:output_type -> device_set.v1.AddDevicesToGroupResponse
+ 24, // 74: device_set.v1.DeviceSetService.RemoveDevicesFromGroup:output_type -> device_set.v1.RemoveDevicesFromGroupResponse
+ 26, // 75: device_set.v1.DeviceSetService.ListDeviceSetMembers:output_type -> device_set.v1.ListDeviceSetMembersResponse
+ 28, // 76: device_set.v1.DeviceSetService.GetDeviceDeviceSets:output_type -> device_set.v1.GetDeviceDeviceSetsResponse
+ 30, // 77: device_set.v1.DeviceSetService.SetRackSlotPosition:output_type -> device_set.v1.SetRackSlotPositionResponse
+ 32, // 78: device_set.v1.DeviceSetService.ClearRackSlotPosition:output_type -> device_set.v1.ClearRackSlotPositionResponse
+ 35, // 79: device_set.v1.DeviceSetService.GetRackSlots:output_type -> device_set.v1.GetRackSlotsResponse
+ 38, // 80: device_set.v1.DeviceSetService.GetDeviceSetStats:output_type -> device_set.v1.GetDeviceSetStatsResponse
+ 41, // 81: device_set.v1.DeviceSetService.ListRackZones:output_type -> device_set.v1.ListRackZonesResponse
+ 43, // 82: device_set.v1.DeviceSetService.ListRackZoneRefs:output_type -> device_set.v1.ListRackZoneRefsResponse
+ 46, // 83: device_set.v1.DeviceSetService.ListRackTypes:output_type -> device_set.v1.ListRackTypesResponse
+ 48, // 84: device_set.v1.DeviceSetService.SaveRack:output_type -> device_set.v1.SaveRackResponse
+ 50, // 85: device_set.v1.DeviceSetService.AssignDevicesToRack:output_type -> device_set.v1.AssignDevicesToRackResponse
+ 68, // [68:86] is the sub-list for method output_type
+ 50, // [50:68] is the sub-list for method input_type
+ 50, // [50:50] is the sub-list for extension type_name
+ 50, // [50:50] is the sub-list for extension extendee
+ 0, // [0:50] is the sub-list for field type_name
}
func init() { file_device_set_v1_device_set_proto_init() }
diff --git a/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go b/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go
index ff1c76bb7c..b4dcdc0e7a 100644
--- a/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go
+++ b/server/generated/grpc/device_set/v1/device_setv1connect/device_set.connect.go
@@ -151,6 +151,15 @@ type DeviceSetServiceClient interface {
// device_selector accepts only the device_list variant. all_devices
// is rejected with InvalidArgument because moving every paired
// device into a single rack is never the intended operation.
+ //
+ // slot_assignments optionally carries each moved device's slot inside
+ // the target rack, so membership and placement land in the same
+ // transaction. This is the rack-level counterpart to
+ // buildings.v1.AssignRacksToBuilding: a delta that names only the
+ // children it changes, with placement optional per child. Prefer it
+ // over SaveRack for every edit — SaveRack replaces the rack's whole
+ // member set, so a stale client snapshot silently drops members
+ // added concurrently.
AssignDevicesToRack(context.Context, *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error)
}
@@ -430,6 +439,15 @@ type DeviceSetServiceHandler interface {
// device_selector accepts only the device_list variant. all_devices
// is rejected with InvalidArgument because moving every paired
// device into a single rack is never the intended operation.
+ //
+ // slot_assignments optionally carries each moved device's slot inside
+ // the target rack, so membership and placement land in the same
+ // transaction. This is the rack-level counterpart to
+ // buildings.v1.AssignRacksToBuilding: a delta that names only the
+ // children it changes, with placement optional per child. Prefer it
+ // over SaveRack for every edit — SaveRack replaces the rack's whole
+ // member set, so a stale client snapshot silently drops members
+ // added concurrently.
AssignDevicesToRack(context.Context, *connect.Request[v1.AssignDevicesToRackRequest]) (*connect.Response[v1.AssignDevicesToRackResponse], error)
}
diff --git a/server/internal/domain/collection/service.go b/server/internal/domain/collection/service.go
index 725301d803..09b314e5b4 100644
--- a/server/internal/domain/collection/service.go
+++ b/server/internal/domain/collection/service.go
@@ -1093,6 +1093,98 @@ type AssignDevicesToRackParams struct {
// site, stripping their site/building to match the rack. When false
// (default) such an add returns Conflicts and writes nothing.
ForceClearConflictingSite bool
+ // SlotAssignments optionally places the assigned devices inside the
+ // target rack's grid, so membership and placement land in the same
+ // transaction. One entry per device whose placement changes: Position
+ // set places it there, Position nil clears its slot. A device not
+ // named here keeps whatever slot it had. Empty writes no slot at all.
+ // Requires TargetRackID. See validateAssignRackSlots.
+ SlotAssignments []*pb.RackSlot
+}
+
+// validateAssignRackSlots enforces the SlotAssignments contract that needs
+// no DB read: a target rack to place into, entries confined to the devices
+// being assigned, and no two entries claiming the same device or the same
+// cell. A nil Position is legal and means "clear this device's slot", the
+// counterpart of buildings.v1.RackPlacement's unset aisle/position. Grid
+// bounds are checked separately, in-tx, once the rack's dimensions are
+// known.
+//
+// The duplicate-position check is the friendly form of
+// uk_rack_slot_position: two entries on one cell would otherwise surface
+// as an opaque constraint violation.
+func validateAssignRackSlots(params AssignDevicesToRackParams) error {
+ if len(params.SlotAssignments) == 0 {
+ return nil
+ }
+ if params.TargetRackID == nil {
+ return fleeterror.NewInvalidArgumentError("slot_assignments requires target_rack_id; an unassign has no rack to place into")
+ }
+ assigned := make(map[string]struct{}, len(params.DeviceIdentifiers))
+ for _, id := range params.DeviceIdentifiers {
+ assigned[id] = struct{}{}
+ }
+ seenDevices := make(map[string]struct{}, len(params.SlotAssignments))
+ seenPositions := make(map[[2]int32]struct{}, len(params.SlotAssignments))
+ for _, slot := range params.SlotAssignments {
+ if slot == nil {
+ return fleeterror.NewInvalidArgumentError("slot assignment must not be empty")
+ }
+ if _, ok := assigned[slot.DeviceIdentifier]; !ok {
+ return fleeterror.NewInvalidArgumentErrorf("slot assignment references device %q which is not in the device selector", slot.DeviceIdentifier)
+ }
+ if _, dup := seenDevices[slot.DeviceIdentifier]; dup {
+ return fleeterror.NewInvalidArgumentErrorf("device %q appears in slot_assignments more than once", slot.DeviceIdentifier)
+ }
+ seenDevices[slot.DeviceIdentifier] = struct{}{}
+ // Unset position = clear the slot; nothing left to bounds check.
+ if slot.Position == nil {
+ continue
+ }
+ if slot.Position.Row < 0 || slot.Position.Column < 0 {
+ return fleeterror.NewInvalidArgumentError("slot position row and column must not be negative")
+ }
+ cell := [2]int32{slot.Position.Row, slot.Position.Column}
+ if _, dup := seenPositions[cell]; dup {
+ return fleeterror.NewInvalidArgumentErrorf("two devices are assigned to slot (%d, %d)", cell[0], cell[1])
+ }
+ seenPositions[cell] = struct{}{}
+ }
+ return nil
+}
+
+// applyRackSlotDelta persists the slot half of an AssignDevicesToRack
+// batch. Only the devices slotAssignments names are touched, so a miner
+// nobody mentioned keeps its slot. That scoping is the whole point of the
+// delta: SaveRack's replace-all shape cannot express "move this one miner"
+// without re-asserting every member.
+//
+// Empty slotAssignments is a no-op, which keeps every pre-existing caller
+// (the importer, the CLI, the overview page's assign-then-place pair)
+// behaving exactly as before: a device already in the target rack retains
+// its slot, and an arriving device is unplaced because its old slot died
+// with its old membership.
+//
+// Otherwise every named device is cleared first, then the entries carrying
+// a position are set. Clearing the whole batch up front is what lets one
+// call swap two occupied cells without tripping uk_rack_slot_position
+// mid-batch, and it is also how an entry with no position expresses
+// "unplace this miner but leave it in the rack".
+func (s *Service) applyRackSlotDelta(ctx context.Context, orgID, rackID int64, slotAssignments []*pb.RackSlot) error {
+ for _, slot := range slotAssignments {
+ if err := s.collectionStore.ClearRackSlotPosition(ctx, rackID, slot.DeviceIdentifier, orgID); err != nil {
+ return err
+ }
+ }
+ for _, slot := range slotAssignments {
+ if slot.Position == nil {
+ continue
+ }
+ if err := s.collectionStore.SetRackSlotPosition(ctx, rackID, slot.DeviceIdentifier, slot.Position.Row, slot.Position.Column, orgID); err != nil {
+ return err
+ }
+ }
+ return nil
}
// PerDeviceRackConflictReason enumerates why a device blocked an
@@ -1147,10 +1239,21 @@ type AssignDevicesToRackResult struct {
//
// Empty DeviceIdentifiers rejects with InvalidArgument so the caller
// learns up-front instead of getting a 0-row response.
+//
+// SlotAssignments, when supplied, places the same devices in the target
+// rack's grid inside this transaction — the rack-level counterpart of
+// AssignRacksToBuilding's optional per-rack aisle/position. That is what
+// lets a client persist a placement edit without re-asserting the rack's
+// whole member set the way SaveRack does.
func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesToRackParams) (*AssignDevicesToRackResult, error) {
if len(params.DeviceIdentifiers) == 0 {
return nil, fleeterror.NewInvalidArgumentError("device_identifiers must not be empty")
}
+ // Shape checks that need no DB read run before the tx opens; the
+ // grid-bounds check needs the rack's dimensions and runs inside.
+ if err := validateAssignRackSlots(params); err != nil {
+ return nil, err
+ }
type txOut struct {
assigned int64
@@ -1165,9 +1268,11 @@ func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesT
}
result, err := s.transactor.RunInTxWithResult(ctx, func(ctx context.Context) (any, error) {
var (
- targetSiteID *int64
- targetBuildingID *int64
- targetLabel string
+ targetSiteID *int64
+ targetBuildingID *int64
+ targetLabel string
+ targetRows, targetColumns int32
+ targetPriorCount int32
)
// Canonical lock order: lock every rack involved in the
// reparent -- sources + target -- together in ascending
@@ -1216,6 +1321,36 @@ func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesT
targetSiteID = placement.SiteID
targetBuildingID = placement.BuildingID
targetLabel = coll.Label
+ // Pre-add member count, read under the rack lock. Paired with
+ // AddDevicesToCollection's newly-inserted count below it gives
+ // the exact resulting size without a second membership read —
+ // RemoveDevicesFromAnyRack excludes the target rack, so nothing
+ // between here and the insert can change this number.
+ targetPriorCount = coll.DeviceCount
+
+ // Read the grid once under the rack lock; it bounds both the
+ // slot positions below and the post-insert capacity check. A
+ // rack with no device_set_rack row yields nil — leave both
+ // guards off rather than inventing dimensions for it.
+ rackInfo, err := s.collectionStore.GetRackInfo(ctx, *params.TargetRackID, params.OrgID)
+ if err != nil {
+ return nil, err
+ }
+ if rackInfo != nil {
+ targetRows, targetColumns = rackInfo.Rows, rackInfo.Columns
+ for _, slot := range params.SlotAssignments {
+ // Unset position = clear; no cell to bounds check.
+ if slot.Position == nil {
+ continue
+ }
+ if slot.Position.Row >= targetRows {
+ return nil, fleeterror.NewInvalidArgumentErrorf("slot row %d is out of bounds (rack has %d rows)", slot.Position.Row, targetRows)
+ }
+ if slot.Position.Column >= targetColumns {
+ return nil, fleeterror.NewInvalidArgumentErrorf("slot column %d is out of bounds (rack has %d columns)", slot.Position.Column, targetColumns)
+ }
+ }
+ }
}
// Placement-consistency guard for a site-less (fully-unassigned)
@@ -1277,6 +1412,25 @@ func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesT
return nil, err
}
newlyAssigned = added
+
+ // Capacity guard. A rack has no "floating" members — every
+ // miner is expected to occupy a slot — so membership is bounded
+ // by rows×columns, the same invariant SaveRack enforces on its
+ // replace path. Without it a delta could push a rack past its
+ // grid and leave miners at positions the grid can never
+ // address.
+ //
+ // prior + newly-inserted is exact: `added` counts only rows the
+ // ON CONFLICT DO NOTHING insert actually created, so devices
+ // already in this rack are not double-counted. Returning an
+ // error rolls the whole tx back, so nothing persists.
+ if capacity := int64(targetRows) * int64(targetColumns); capacity > 0 {
+ if resulting := int64(targetPriorCount) + added; resulting > capacity {
+ return nil, fleeterror.NewInvalidArgumentErrorf(
+ "cannot assign %d miner(s) to rack %q: it would hold %d miner(s) but has only %d slot(s) (%d×%d)",
+ len(uniqueIdentifiers(params.DeviceIdentifiers)), targetLabel, resulting, capacity, targetRows, targetColumns)
+ }
+ }
assigned = int64(len(uniqueIdentifiers(params.DeviceIdentifiers)))
// Site cascade fires when the rack has a site OR a building.
// A rack in a building inherits that building's site (NULL
@@ -1328,6 +1482,12 @@ func (s *Service) AssignDevicesToRack(ctx context.Context, params AssignDevicesT
}
siteReassigned = stripped
}
+
+ // Placement, last: membership must exist before a slot can
+ // reference it (both slot queries join device_set_membership).
+ if err := s.applyRackSlotDelta(ctx, params.OrgID, *params.TargetRackID, params.SlotAssignments); err != nil {
+ return nil, err
+ }
}
return &txOut{
diff --git a/server/internal/domain/collection/service_test.go b/server/internal/domain/collection/service_test.go
index dbbe9d3e64..a2924232ef 100644
--- a/server/internal/domain/collection/service_test.go
+++ b/server/internal/domain/collection/service_test.go
@@ -2,6 +2,7 @@ package collection
import (
"context"
+ "fmt"
"testing"
pb "github.com/block/proto-fleet/server/generated/grpc/collection/v1"
@@ -2611,6 +2612,8 @@ func TestService_AssignDevicesToRack_atomicReassign(t *testing.T) {
Return(interfaces.RackPlacement{SiteID: &rackSite}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(2), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(2), nil),
mockStore.EXPECT().GetDeviceSiteIDsByMembership(gomock.Any(), targetRackID, testOrgID).
@@ -2698,6 +2701,8 @@ func TestService_AssignDevicesToRack_acquiresSourceRackLocksBeforeWrites(t *test
Return(interfaces.RackPlacement{}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().FindDevicesWithSiteOrBuilding(gomock.Any(), testOrgID, deviceIDs).Return(nil, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(2), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(2), nil),
@@ -2761,6 +2766,8 @@ func TestService_AssignDevicesToRack_locksIncludeTargetRack(t *testing.T) {
Return(interfaces.RackPlacement{}, nil)
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-Target", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil)
// Site-less target rack → site-consistency pre-check + post-add strip.
mockStore.EXPECT().FindDevicesWithSiteOrBuilding(gomock.Any(), testOrgID, deviceIDs).Return(nil, nil)
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(0), nil)
@@ -2791,6 +2798,8 @@ func TestService_AssignDevicesToRack_siteLessRackConflictNoForce(t *testing.T) {
mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID).Return(interfaces.RackPlacement{}, nil)
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-Target", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil)
// d1 currently has a site → would be stripped. No force → reject,
// no RemoveDevicesFromAnyRack / AddDevicesToCollection.
mockStore.EXPECT().FindDevicesWithSiteOrBuilding(gomock.Any(), testOrgID, deviceIDs).Return([]string{"d1"}, nil)
@@ -2822,6 +2831,8 @@ func TestService_AssignDevicesToRack_siteLessRackForceStrips(t *testing.T) {
mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID).Return(interfaces.RackPlacement{}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-Target", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().FindDevicesWithSiteOrBuilding(gomock.Any(), testOrgID, deviceIDs).Return([]string{"d1"}, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(0), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil),
@@ -2855,6 +2866,336 @@ func TestService_AssignDevicesToRack_emptyDevicesRejected(t *testing.T) {
require.Error(t, err)
}
+// rackSlot is a terse constructor for the slot-delta tests below.
+func rackSlot(deviceIdentifier string, row, column int32) *pb.RackSlot {
+ return &pb.RackSlot{
+ DeviceIdentifier: deviceIdentifier,
+ Position: &pb.RackSlotPosition{Row: row, Column: column},
+ }
+}
+
+// expectAssignToRackPreamble sets up the locks + reads every rack-target
+// assign performs before it writes, so the slot tests can assert only on
+// the slot half. Placement is a plain site so the site-strip pre-check
+// doesn't fire.
+func expectAssignToRackPreamble(mockStore *mocks.MockCollectionStore, rackID int64, deviceIDs []string, rows, columns int32) {
+ site := int64(7)
+ mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, rackID).Return(nil, nil)
+ mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, testOrgID).
+ Return(interfaces.RackPlacement{SiteID: &site}, nil)
+ mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, rackID).
+ Return(&pb.DeviceCollection{Id: rackID, Label: "Rack-A", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), rackID, testOrgID).
+ Return(&pb.RackInfo{Rows: rows, Columns: columns}, nil)
+ mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, rackID).Return(int64(0), nil)
+ mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(len(deviceIDs)), nil)
+ mockStore.EXPECT().GetDeviceSiteIDsByMembership(gomock.Any(), rackID, testOrgID).Return(nil, nil)
+ mockStore.EXPECT().CascadeAddedDeviceSites(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(0), nil)
+ mockStore.EXPECT().CascadeAddedDeviceBuildings(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(0), nil)
+}
+
+// TestService_AssignDevicesToRack_slotDeltaClearsThenSets pins the core of
+// the placement delta: every named device is cleared before any position
+// is written. Without that ordering a relayout that swaps two occupied
+// cells trips uk_rack_slot_position mid-batch.
+func TestService_AssignDevicesToRack_slotDeltaClearsThenSets(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1", "d2"}
+ expectAssignToRackPreamble(mockStore, rackID, deviceIDs, 10, 10)
+
+ // Both clears must precede both sets. gomock.InOrder can't express
+ // "any order within a group", so assert the boundary directly: record
+ // the call sequence and check no clear follows a set.
+ var calls []string
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, gomock.Any(), testOrgID).Times(2).
+ DoAndReturn(func(_ context.Context, _ int64, id string, _ int64) error {
+ calls = append(calls, "clear:"+id)
+ return nil
+ })
+ mockStore.EXPECT().SetRackSlotPosition(gomock.Any(), rackID, gomock.Any(), gomock.Any(), gomock.Any(), testOrgID).Times(2).
+ DoAndReturn(func(_ context.Context, _ int64, id string, row, column int32, _ int64) error {
+ calls = append(calls, fmt.Sprintf("set:%s@%d,%d", id, row, column))
+ return nil
+ })
+
+ // d1 and d2 swap cells.
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 1), rackSlot("d2", 0, 0)},
+ })
+ require.NoError(t, err)
+
+ require.Len(t, calls, 4)
+ assert.ElementsMatch(t, []string{"clear:d1", "clear:d2"}, calls[:2], "both clears must land first")
+ assert.ElementsMatch(t, []string{"set:d1@0,1", "set:d2@0,0"}, calls[2:])
+}
+
+// TestService_AssignDevicesToRack_slotDeltaUnplacesOnNilPosition pins the
+// mixed placed+unplaced case that makes this a delta rather than a
+// replace: an entry with no position clears that device's slot — the
+// counterpart of RackPlacement's unset aisle/position — which is how a
+// caller says "pull this miner off the grid but leave it in the rack".
+func TestService_AssignDevicesToRack_slotDeltaUnplacesOnNilPosition(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1", "d2"}
+ expectAssignToRackPreamble(mockStore, rackID, deviceIDs, 10, 10)
+
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d1", testOrgID).Return(nil)
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d2", testOrgID).Return(nil)
+ // Only d1 carries a position; d2's nil-position entry leaves it cleared.
+ mockStore.EXPECT().SetRackSlotPosition(gomock.Any(), rackID, "d1", int32(2), int32(3), testOrgID).Return(nil)
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 2, 3), {DeviceIdentifier: "d2"}},
+ })
+ require.NoError(t, err)
+}
+
+// TestService_AssignDevicesToRack_slotDeltaPureUnplace covers the case an
+// "empty list means clear the selector" rule could not express at all: the
+// operator pulls every miner they touched off the grid. Each is named with
+// no position, so the batch is non-empty and the clears actually land.
+func TestService_AssignDevicesToRack_slotDeltaPureUnplace(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1", "d2"}
+ expectAssignToRackPreamble(mockStore, rackID, deviceIDs, 10, 10)
+
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d1", testOrgID).Return(nil)
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d2", testOrgID).Return(nil)
+ // No SetRackSlotPosition expectation: the strict mock fails if one fires.
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{{DeviceIdentifier: "d1"}, {DeviceIdentifier: "d2"}},
+ })
+ require.NoError(t, err)
+}
+
+// TestService_AssignDevicesToRack_slotDeltaLeavesUnnamedDeviceAlone is the
+// invariant that makes this safe where SaveRack was not: a miner the batch
+// never mentions keeps its slot, even though it is a member of the same
+// rack. SaveRack could only express this by re-asserting the entire member
+// set from the client's (possibly stale) snapshot.
+func TestService_AssignDevicesToRack_slotDeltaLeavesUnnamedDeviceAlone(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ // d2 is in the selector but absent from slot_assignments.
+ deviceIDs := []string{"d1", "d2"}
+ expectAssignToRackPreamble(mockStore, rackID, deviceIDs, 10, 10)
+
+ // Only d1 is cleared and re-placed. A Clear on d2 fails the strict mock.
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d1", testOrgID).Return(nil)
+ mockStore.EXPECT().SetRackSlotPosition(gomock.Any(), rackID, "d1", int32(0), int32(0), testOrgID).Return(nil)
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 0)},
+ })
+ require.NoError(t, err)
+}
+
+// TestService_AssignDevicesToRack_noSlotsLeavesPlacementUntouched pins the
+// backward-compatible default every pre-existing caller relies on (the
+// importer, the CLI, the overview page's assign-then-place pair): an empty
+// SlotAssignments touches no slot row at all, so a device already in the
+// target rack keeps the slot it had.
+func TestService_AssignDevicesToRack_noSlotsLeavesPlacementUntouched(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1"}
+ expectAssignToRackPreamble(mockStore, rackID, deviceIDs, 10, 10)
+ // No ClearRackSlotPosition / SetRackSlotPosition expectations: the
+ // strict mock fails the test if either fires.
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ })
+ require.NoError(t, err)
+}
+
+// TestService_AssignDevicesToRack_slotDeltaRejectsBadInput covers the
+// shape contract. Each case must reject BEFORE any store call, so these
+// run against a mock with zero expectations — a write slipping through
+// would fail on the unexpected call rather than pass silently.
+func TestService_AssignDevicesToRack_slotDeltaRejectsBadInput(t *testing.T) {
+ rackID := int64(42)
+ cases := []struct {
+ name string
+ params AssignDevicesToRackParams
+ }{
+ {
+ // A device the caller never asked to assign has no membership
+ // row for the slot to hang off, so placing it is meaningless.
+ name: "slot for a device outside the selector",
+ params: AssignDevicesToRackParams{
+ TargetRackID: &rackID,
+ DeviceIdentifiers: []string{"d1"},
+ SlotAssignments: []*pb.RackSlot{rackSlot("stranger", 0, 0)},
+ },
+ },
+ {
+ // Would trip uk_rack_slot_position; reject with a readable
+ // message instead of an opaque constraint violation.
+ name: "two devices on one cell",
+ params: AssignDevicesToRackParams{
+ TargetRackID: &rackID,
+ DeviceIdentifiers: []string{"d1", "d2"},
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 1, 1), rackSlot("d2", 1, 1)},
+ },
+ },
+ {
+ // Ambiguous: the last entry would silently win.
+ name: "one device in two cells",
+ params: AssignDevicesToRackParams{
+ TargetRackID: &rackID,
+ DeviceIdentifiers: []string{"d1"},
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 0), rackSlot("d1", 1, 1)},
+ },
+ },
+ {
+ // An unassign has no rack to place into.
+ name: "slots without a target rack",
+ params: AssignDevicesToRackParams{
+ DeviceIdentifiers: []string{"d1"},
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 0)},
+ },
+ },
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ svc, _, _ := newTestServiceWithSites(t, nil)
+ tc.params.OrgID = testOrgID
+ _, err := svc.AssignDevicesToRack(testCtx(t), tc.params)
+ require.Error(t, err)
+ })
+ }
+}
+
+// TestService_AssignDevicesToRack_slotOutOfGridRejected pins the bounds
+// check that needs the rack's dimensions, so it runs in-tx after the grid
+// read rather than in the pre-tx shape validation.
+func TestService_AssignDevicesToRack_slotOutOfGridRejected(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1"}
+ site := int64(7)
+ // Only the reads up to the grid read: the rejection must land before
+ // any write, so no Remove/Add expectations are declared.
+ mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, rackID).Return(nil, nil)
+ mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, testOrgID).
+ Return(interfaces.RackPlacement{SiteID: &site}, nil)
+ mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, rackID).
+ Return(&pb.DeviceCollection{Id: rackID, Label: "Rack-A", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), rackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 2, Columns: 2}, nil)
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 5)},
+ })
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "out of bounds")
+}
+
+// TestService_AssignDevicesToRack_overCapacityRejected pins the capacity
+// invariant SaveRack already enforces on its replace path. A rack has no
+// floating members, so a delta must not push membership past rows×columns
+// and leave miners at positions the grid can never address.
+func TestService_AssignDevicesToRack_overCapacityRejected(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d3", "d4"}
+ site := int64(7)
+ mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, rackID).Return(nil, nil)
+ mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, testOrgID).
+ Return(interfaces.RackPlacement{SiteID: &site}, nil)
+ // 1×3 grid already holding 2 miners.
+ mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, rackID).
+ Return(&pb.DeviceCollection{Id: rackID, Label: "Rack-A", Type: pb.CollectionType_COLLECTION_TYPE_RACK, DeviceCount: 2}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), rackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 1, Columns: 3}, nil)
+ mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, rackID).Return(int64(0), nil)
+ // Both inserts are new → 2 + 2 = 4 in a 3-slot rack.
+ mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(2), nil)
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ })
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "slot(s)")
+}
+
+// TestService_AssignDevicesToRack_reAssertingMembersIsNotOverCapacity
+// guards the counting rule: capacity uses the newly-INSERTED count, not
+// the requested count, so re-asserting devices already in a full rack (as
+// a pure placement edit does) must not read as an overflow.
+func TestService_AssignDevicesToRack_reAssertingMembersIsNotOverCapacity(t *testing.T) {
+ svc, mockStore, _ := newTestServiceWithSites(t, nil)
+ ctx := testCtx(t)
+
+ rackID := int64(42)
+ deviceIDs := []string{"d1", "d2"}
+ site := int64(7)
+ mockStore.EXPECT().LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, rackID).Return(nil, nil)
+ mockStore.EXPECT().LockRackPlacementForWrite(gomock.Any(), rackID, testOrgID).
+ Return(interfaces.RackPlacement{SiteID: &site}, nil)
+ // 1×2 grid, already exactly full with these same two miners.
+ mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, rackID).
+ Return(&pb.DeviceCollection{Id: rackID, Label: "Rack-A", Type: pb.CollectionType_COLLECTION_TYPE_RACK, DeviceCount: 2}, nil)
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), rackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 1, Columns: 2}, nil)
+ mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, rackID).Return(int64(0), nil)
+ // Already members → ON CONFLICT DO NOTHING inserts nothing.
+ mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(0), nil)
+ mockStore.EXPECT().GetDeviceSiteIDsByMembership(gomock.Any(), rackID, testOrgID).Return(nil, nil)
+ mockStore.EXPECT().CascadeAddedDeviceSites(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(0), nil)
+ mockStore.EXPECT().CascadeAddedDeviceBuildings(gomock.Any(), testOrgID, rackID, deviceIDs).Return(int64(0), nil)
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d1", testOrgID).Return(nil)
+ mockStore.EXPECT().ClearRackSlotPosition(gomock.Any(), rackID, "d2", testOrgID).Return(nil)
+ mockStore.EXPECT().SetRackSlotPosition(gomock.Any(), rackID, "d1", int32(0), int32(1), testOrgID).Return(nil)
+ mockStore.EXPECT().SetRackSlotPosition(gomock.Any(), rackID, "d2", int32(0), int32(0), testOrgID).Return(nil)
+
+ _, err := svc.AssignDevicesToRack(ctx, AssignDevicesToRackParams{
+ OrgID: testOrgID,
+ TargetRackID: &rackID,
+ DeviceIdentifiers: deviceIDs,
+ SlotAssignments: []*pb.RackSlot{rackSlot("d1", 0, 1), rackSlot("d2", 0, 0)},
+ })
+ require.NoError(t, err)
+}
+
// TestService_AssignDevicesToRack_crossSiteEmitsCascadeMetadata asserts
// that when the target rack lives at a different site than (some of) the
// devices, the activity event carries SiteID + cascade Metadata mirroring
@@ -2877,6 +3218,8 @@ func TestService_AssignDevicesToRack_crossSiteEmitsCascadeMetadata(t *testing.T)
Return(interfaces.RackPlacement{SiteID: &rackSite}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(1), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil),
mockStore.EXPECT().GetDeviceSiteIDsByMembership(gomock.Any(), targetRackID, testOrgID).
@@ -2931,6 +3274,8 @@ func TestService_AssignDevicesToRack_sameSiteSkipsCascadeMetadata(t *testing.T)
Return(interfaces.RackPlacement{SiteID: &rackSite}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(0), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil),
mockStore.EXPECT().GetDeviceSiteIDsByMembership(gomock.Any(), targetRackID, testOrgID).
@@ -2980,6 +3325,8 @@ func TestService_AssignDevicesToRack_siteLessBuildingClearsDeviceSite(t *testing
Return(interfaces.RackPlacement{SiteID: nil, BuildingID: &rackBuilding}, nil),
mockStore.EXPECT().GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&pb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: pb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ mockStore.EXPECT().GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&pb.RackInfo{Rows: 10, Columns: 10}, nil),
mockStore.EXPECT().RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).Return(int64(1), nil),
mockStore.EXPECT().AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).Return(int64(1), nil),
// Site cascade now fires even though targetSiteID is nil, because
diff --git a/server/internal/handlers/deviceset/handler_test.go b/server/internal/handlers/deviceset/handler_test.go
index 7808561e7c..5546a783e3 100644
--- a/server/internal/handlers/deviceset/handler_test.go
+++ b/server/internal/handlers/deviceset/handler_test.go
@@ -893,6 +893,9 @@ func TestAssignDevicesToRack_HappyPathAssigns(t *testing.T) {
h.collectionStore.EXPECT().
GetCollection(gomock.Any(), testOrgID, targetRackID).
Return(&collectionpb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: collectionpb.CollectionType_COLLECTION_TYPE_RACK}, nil),
+ h.collectionStore.EXPECT().
+ GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&collectionpb.RackInfo{Rows: 10, Columns: 10}, nil),
h.collectionStore.EXPECT().
RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).
Return(int64(2), nil),
@@ -949,6 +952,87 @@ func TestAssignDevicesToRack_UnassignBranch(t *testing.T) {
assert.Equal(t, int64(0), resp.Msg.SiteReassignedCount)
}
+// TestAssignDevicesToRack_CarriesSlotAssignments pins the wire→domain
+// mapping for the placement half of the delta: slot_assignments must reach
+// the store as real slot writes in the same call that moves membership.
+// That is what lets the rack modals persist a placement edit without
+// SaveRack's replace-all member set.
+func TestAssignDevicesToRack_CarriesSlotAssignments(t *testing.T) {
+ h := newTestHandler(t)
+
+ targetRackID := int64(42)
+ rackSite := int64(7)
+ deviceIDs := []string{"d1", "d2"}
+
+ h.collectionStore.EXPECT().
+ LockRacksForReparent(gomock.Any(), testOrgID, deviceIDs, targetRackID).
+ Return([]int64{targetRackID}, nil)
+ h.collectionStore.EXPECT().
+ LockRackPlacementForWrite(gomock.Any(), targetRackID, testOrgID).
+ Return(interfaces.RackPlacement{SiteID: &rackSite}, nil)
+ h.collectionStore.EXPECT().
+ GetCollection(gomock.Any(), testOrgID, targetRackID).
+ Return(&collectionpb.DeviceCollection{Id: targetRackID, Label: "Rack-B", Type: collectionpb.CollectionType_COLLECTION_TYPE_RACK}, nil)
+ h.collectionStore.EXPECT().
+ GetRackInfo(gomock.Any(), targetRackID, testOrgID).
+ Return(&collectionpb.RackInfo{Rows: 4, Columns: 4}, nil)
+ h.collectionStore.EXPECT().
+ RemoveDevicesFromAnyRack(gomock.Any(), testOrgID, deviceIDs, targetRackID).
+ Return(int64(0), nil)
+ h.collectionStore.EXPECT().
+ AddDevicesToCollection(gomock.Any(), testOrgID, targetRackID, deviceIDs).
+ Return(int64(2), nil)
+ h.collectionStore.EXPECT().
+ GetDeviceSiteIDsByMembership(gomock.Any(), targetRackID, testOrgID).
+ Return(nil, nil)
+ h.collectionStore.EXPECT().
+ CascadeAddedDeviceSites(gomock.Any(), testOrgID, targetRackID, deviceIDs).
+ Return(int64(0), nil)
+ h.collectionStore.EXPECT().
+ CascadeAddedDeviceBuildings(gomock.Any(), testOrgID, targetRackID, deviceIDs).
+ Return(int64(0), nil)
+ // d1 is placed at (1,2). d2 is named with no position, so it is cleared
+ // and left off the grid — the wire form of "in the rack, unplaced".
+ h.collectionStore.EXPECT().ClearRackSlotPosition(gomock.Any(), targetRackID, "d1", testOrgID).Return(nil)
+ h.collectionStore.EXPECT().ClearRackSlotPosition(gomock.Any(), targetRackID, "d2", testOrgID).Return(nil)
+ h.collectionStore.EXPECT().
+ SetRackSlotPosition(gomock.Any(), targetRackID, "d1", int32(1), int32(2), testOrgID).
+ Return(nil)
+
+ resp, err := h.handler.AssignDevicesToRack(testCtx(t), connect.NewRequest(&dspb.AssignDevicesToRackRequest{
+ TargetRackId: &targetRackID,
+ DeviceSelector: deviceListSelector(deviceIDs...),
+ SlotAssignments: []*dspb.RackSlot{
+ {DeviceIdentifier: "d1", Position: &dspb.RackSlotPosition{Row: 1, Column: 2}},
+ {DeviceIdentifier: "d2"},
+ },
+ }))
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), resp.Msg.AssignedCount)
+}
+
+// TestAssignDevicesToRack_RejectsSlotOutsideSelector confirms the handler
+// forwards enough for the domain to reject a slot naming a device the
+// caller never asked to assign — it would have no membership row to hang
+// off. Rejected before any store call.
+func TestAssignDevicesToRack_RejectsSlotOutsideSelector(t *testing.T) {
+ h := newTestHandler(t)
+
+ resp, err := h.handler.AssignDevicesToRack(testCtx(t), connect.NewRequest(&dspb.AssignDevicesToRackRequest{
+ TargetRackId: ptrInt64Local(42),
+ DeviceSelector: deviceListSelector("d1"),
+ SlotAssignments: []*dspb.RackSlot{{
+ DeviceIdentifier: "stranger",
+ Position: &dspb.RackSlotPosition{Row: 0, Column: 0},
+ }},
+ }))
+ require.Error(t, err)
+ require.Nil(t, resp)
+ var fe fleeterror.FleetError
+ require.ErrorAs(t, err, &fe)
+ assert.Equal(t, connect.CodeInvalidArgument, fe.GRPCCode)
+}
+
// TestAssignDevicesToRack_RejectsAllDevicesSelector confirms that the
// all_devices selector variant is rejected with InvalidArgument at the
// handler boundary, before any store call fires. Moving every paired
diff --git a/server/internal/handlers/deviceset/translate.go b/server/internal/handlers/deviceset/translate.go
index 816fed69b0..45bc04151c 100644
--- a/server/internal/handlers/deviceset/translate.go
+++ b/server/internal/handlers/deviceset/translate.go
@@ -27,12 +27,16 @@ func toAssignDevicesToRackParams(req *dspb.AssignDevicesToRackRequest, orgID int
if err != nil {
return collection.AssignDevicesToRackParams{}, err
}
- return collection.AssignDevicesToRackParams{
+ params := collection.AssignDevicesToRackParams{
OrgID: orgID,
TargetRackID: targetRackID,
DeviceIdentifiers: identifiers,
ForceClearConflictingSite: req.GetForceClearConflictingSite(),
- }, nil
+ }
+ for _, sa := range req.GetSlotAssignments() {
+ params.SlotAssignments = append(params.SlotAssignments, toCollectionRackSlot(sa))
+ }
+ return params, nil
}
// toProtoRackConflicts maps domain add-to-rack conflicts onto the proto