Skip to content

Commit

Permalink
DEVPROD-12285: Allow users to modify volume size (#656)
Browse files Browse the repository at this point in the history
  • Loading branch information
minnakt authored Feb 27, 2025
1 parent ebbe17d commit 7944801
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 26 deletions.
27 changes: 26 additions & 1 deletion apps/spruce/cypress/integration/spawn/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe("Spawn volume page", () => {
cy.dataCy("update-volume-modal").should("be.visible");
});

it("Volume name & expiration inputs should be populated with the volume display name & expiration on initial render", () => {
it("name, size, expiration inputs should be populated on initial render", () => {
cy.dataCy(
"edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858",
).click();
Expand All @@ -160,6 +160,7 @@ describe("Spawn volume page", () => {
"have.value",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858",
);
cy.dataCy("volume-size-input").should("have.value", "100");
cy.dataCy("date-picker").should("have.value", "2020-06-06");
cy.dataCy("time-picker").should("have.value", "15:48:18"); // Defaults to UTC
});
Expand All @@ -182,6 +183,30 @@ describe("Spawn volume page", () => {
);
});

it("size field is validated correctly", () => {
cy.dataCy(
"edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858",
).click();
cy.dataCy("update-volume-modal").should("be.visible");

// Exceeding max volume limit should disable 'Save' button.
cy.dataCy("volume-size-input").clear();
cy.dataCy("volume-size-input").type("10000");
cy.contains("button", "Save").should(
"have.attr",
"aria-disabled",
"true",
);
// Decreasing volume size should disable 'Save' button.
cy.dataCy("volume-size-input").clear();
cy.dataCy("volume-size-input").type("2");
cy.contains("button", "Save").should(
"have.attr",
"aria-disabled",
"true",
);
});

it("Submit button should be enabled when the volume details input value differs from what already exists.", () => {
cy.dataCy(
"edit-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@ import { ExpirationRow } from "../ExpirationRow";
interface Props {
disableExpirationCheckbox: boolean;
hasName: boolean;
maxSpawnableLimit: number;
minVolumeSize: number;
noExpirationCheckboxTooltip: string;
}

export const getFormSchema = ({
disableExpirationCheckbox,
hasName,
maxSpawnableLimit,
minVolumeSize,
noExpirationCheckboxTooltip,
}: Props): ReturnType<GetFormSchema> => ({
fields: {},
Expand All @@ -24,6 +28,12 @@ export const getFormSchema = ({
// The back end requires a name if one has previously been set, so prevent users from unsetting a name.
...(hasName && { minLength: 1 }),
},
size: {
type: "number",
title: "Volume Size (GiB)",
minimum: minVolumeSize,
maximum: maxSpawnableLimit,
},
expirationDetails: {
type: "object",
properties: {
Expand Down Expand Up @@ -69,6 +79,10 @@ export const getFormSchema = ({
name: {
"ui:data-cy": "volume-name-input",
},
size: {
"ui:data-cy": "volume-size-input",
"ui:description": `The max volume size is ${maxSpawnableLimit} GiB. Volume size can only be updated once every 6 hours, and cannot be decreased.`,
},
expirationDetails: {
"ui:ObjectFieldTemplate": ExpirationRow,
expiration: {
Expand All @@ -79,7 +93,7 @@ export const getFormSchema = ({
},
noExpiration: {
"ui:disabled": disableExpirationCheckbox,
"ui:tooltipDescription": noExpirationCheckboxTooltip ?? "",
"ui:tooltipDescription": noExpirationCheckboxTooltip,
"ui:elementWrapperCSS": checkboxCSS,
},
},
Expand Down
11 changes: 6 additions & 5 deletions apps/spruce/src/components/Spawn/editVolumeModal/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@ export const formToGql = (
volumeId: string,
) => {
const updatedFields: Partial<FormState> = diff(initialState, formData);
const { expirationDetails, name = "", size } = updatedFields;

const {
expirationDetails = {} as FormState["expirationDetails"],
name = "",
} = updatedFields;
// @ts-expect-error: FIXME. This comment was added by an automated script.
const { expiration, noExpiration } = expirationDetails;
expiration = formData.expirationDetails?.expiration,
noExpiration = formData.expirationDetails?.noExpiration,
} = expirationDetails ?? {};

return {
...(noExpiration && { noExpiration }),
...(expiration && !noExpiration && { expiration: new Date(expiration) }),
...(name && { name }),
...(size && { size }),
volumeId,
};
};
1 change: 1 addition & 0 deletions apps/spruce/src/components/Spawn/editVolumeModal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export type FormState = {
expiration?: string;
noExpiration: boolean;
};
size?: number;
};
5 changes: 4 additions & 1 deletion apps/spruce/src/pages/spawn/SpawnVolume.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export const SpawnVolume = () => {
volumeLimit={volumeLimit}
/>
{volumes.length ? (
<SpawnVolumeTable volumes={volumes} />
<SpawnVolumeTable
maxSpawnableLimit={maxSpawnableLimit}
volumes={volumes}
/>
) : (
<Subtitle>No volumes available, spawn one to get started.</Subtitle>
)}
Expand Down
16 changes: 14 additions & 2 deletions apps/spruce/src/pages/spawn/spawnVolume/SpawnVolumeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import { SpawnVolumeTableActions } from "./SpawnVolumeTableActions";
import { VolumeStatusBadge } from "./VolumeStatusBadge";

interface SpawnVolumeTableProps {
maxSpawnableLimit: number;
volumes: MyVolume[];
}

export const SpawnVolumeTable: React.FC<SpawnVolumeTableProps> = ({
maxSpawnableLimit,
volumes,
}) => {
const [selectedVolume] = useQueryParam(QueryParams.Volume, "");
Expand All @@ -38,6 +40,11 @@ export const SpawnVolumeTable: React.FC<SpawnVolumeTableProps> = ({
dataSource.map(({ id }, i) => [i, id === selectedVolume]),
);

const columns = useMemo(
() => getColumns(maxSpawnableLimit),
[maxSpawnableLimit],
);

const tableContainerRef = useRef<HTMLDivElement>(null);
const table = useLeafyGreenTable<TableVolume>({
columns,
Expand Down Expand Up @@ -65,7 +72,7 @@ const getHostDisplayName = (v: TableVolume) =>
const sortByHost = (a: TableVolume, b: TableVolume) =>
getHostDisplayName(a).localeCompare(getHostDisplayName(b));

const columns = [
const getColumns = (maxSpawnableLimit: number) => [
{
header: "Volume",
// @ts-expect-error: FIXME. This comment was added by an automated script.
Expand Down Expand Up @@ -134,7 +141,12 @@ const columns = [
{
header: "Actions",
// @ts-expect-error: FIXME. This comment was added by an automated script.
cell: ({ row }) => <SpawnVolumeTableActions volume={row.original} />,
cell: ({ row }) => (
<SpawnVolumeTableActions
maxSpawnableLimit={maxSpawnableLimit}
volume={row.original}
/>
),
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ import { MountButton } from "./spawnVolumeTableActions/MountButton";
import { UnmountButton } from "./spawnVolumeTableActions/UnmountButton";

interface Props {
maxSpawnableLimit: number;
volume: TableVolume;
}

export const SpawnVolumeTableActions: React.FC<Props> = ({ volume }) => {
export const SpawnVolumeTableActions: React.FC<Props> = ({
maxSpawnableLimit,
volume,
}) => {
const { displayName, homeVolume, host, id } = volume;
return (
<FlexRow
Expand All @@ -33,7 +37,11 @@ export const SpawnVolumeTableActions: React.FC<Props> = ({ volume }) => {
{!host && !homeVolume && (
<MountButton data-cy={`mount-${displayName || id}`} volume={volume} />
)}
<EditButton data-cy={`edit-${displayName || id}`} volume={volume} />
<EditButton
data-cy={`edit-${displayName || id}`}
maxSpawnableLimit={maxSpawnableLimit}
volume={volume}
/>
</FlexRow>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { TableVolume } from "types/spawn";
import { EditVolumeModal } from "./EditVolumeModal";

interface Props {
maxSpawnableLimit: number;
volume: TableVolume;
}

export const EditButton: React.FC<Props> = ({ volume }) => {
export const EditButton: React.FC<Props> = ({ maxSpawnableLimit, volume }) => {
const [openModal, setOpenModal] = useState(false);

return (
Expand All @@ -25,6 +26,7 @@ export const EditButton: React.FC<Props> = ({ volume }) => {
</Button>
{openModal && (
<EditVolumeModal
maxSpawnableLimit={maxSpawnableLimit}
onCancel={() => setOpenModal(false)}
visible={openModal}
volume={volume}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo, useState } from "react";
import { useMutation } from "@apollo/client";
import { AjvError } from "@rjsf/core";
import { diff } from "deep-object-diff";
import { useToastContext } from "@evg-ui/lib/context/toast";
import { useSpawnAnalytics } from "analytics";
Expand All @@ -19,12 +20,14 @@ import { UPDATE_SPAWN_VOLUME } from "gql/mutations";
import { TableVolume } from "types/spawn";

interface Props {
visible: boolean;
maxSpawnableLimit: number;
onCancel: () => void;
visible: boolean;
volume: TableVolume;
}

export const EditVolumeModal: React.FC<Props> = ({
maxSpawnableLimit,
onCancel,
visible,
volume,
Expand Down Expand Up @@ -52,36 +55,39 @@ export const EditVolumeModal: React.FC<Props> = ({
const initialState = useMemo(
() => ({
expirationDetails: {
// @ts-expect-error: FIXME. This comment was added by an automated script.
expiration: new Date(volume?.expiration).toString(),
expiration: volume?.expiration
? new Date(volume?.expiration).toString()
: undefined,
noExpiration: volume.noExpiration,
},
name: volume.displayName,
size: volume.size,
}),
[volume],
);
const [formState, setFormState] = useState<FormState>(initialState);
const [formErrors, setFormErrors] = useState([]);
const [formErrors, setFormErrors] = useState<AjvError[]>([]);

const updateVolume = () => {
const mutationInput = formToGql(initialState, formState, volume.id);
spawnAnalytics.sendEvent({
name: "Changed spawn volume settings",
"volume.is_unexpirable": mutationInput.noExpiration,
"volume.is_unexpirable": mutationInput.noExpiration ?? false,
});
updateVolumeMutation({
variables: { updateVolumeInput: mutationInput },
});
};

const { disableExpirationCheckbox, noExpirationCheckboxTooltip } =
const { disableExpirationCheckbox, noExpirationCheckboxTooltip = "" } =
useLoadFormData(volume);

const { schema, uiSchema } = getFormSchema({
maxSpawnableLimit,
minVolumeSize: volume.size,
disableExpirationCheckbox,
// @ts-expect-error: FIXME. This comment was added by an automated script.
noExpirationCheckboxTooltip,
hasName: !!initialState?.name?.length,
hasName: initialState?.name?.length > 0,
});

const hasChanges = useMemo(() => {
Expand All @@ -91,19 +97,22 @@ export const EditVolumeModal: React.FC<Props> = ({

return (
<ConfirmationModal
buttonText={loading ? "Saving" : "Save"}
cancelButtonProps={{
onClick: onCancel,
}}
confirmButtonProps={{
children: loading ? "Saving" : "Save",
disabled: loading || !hasChanges || !!formErrors.length,
onClick: updateVolume,
}}
data-cy="update-volume-modal"
onCancel={onCancel}
onConfirm={updateVolume}
open={visible}
submitDisabled={loading || !hasChanges || !!formErrors.length}
title="Edit Volume"
>
<SpruceForm
formData={formState}
onChange={({ errors, formData }) => {
setFormState(formData);
// @ts-expect-error: FIXME. This comment was added by an automated script.
setFormErrors(errors);
}}
schema={schema}
Expand Down

0 comments on commit 7944801

Please sign in to comment.