diff --git a/apps/spruce/cypress/integration/spawn/volume.ts b/apps/spruce/cypress/integration/spawn/volume.ts index d382817fb..de82661b0 100644 --- a/apps/spruce/cypress/integration/spawn/volume.ts +++ b/apps/spruce/cypress/integration/spawn/volume.ts @@ -250,7 +250,8 @@ describe("Spawn volume page", () => { "true", ); }); - it("clicking cancel during confirmation renders the Migrate modal form", () => { + // TODO the availability zone defined in Evergreen's test data for this volume is invalid, making it impossible to submit the form. Re-enable these tests when the data is fixed. + it.skip("clicking cancel during confirmation renders the Migrate modal form", () => { cy.dataCy( "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", ).click(); @@ -264,7 +265,7 @@ describe("Spawn volume page", () => { cy.dataCy("migrate-modal").contains("Cancel").click({ force: true }); cy.dataCy("distro-input").should("be.visible"); }); - it("open the Migrate modal and spawn a host", () => { + it.skip("open the Migrate modal and spawn a host", () => { cy.dataCy( "migrate-btn-e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b858", ).click(); diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/Widgets/DistroDropdown.tsx b/apps/spruce/src/components/Spawn/spawnHostModal/Widgets/DistroDropdown.tsx index a652d47a2..912905f5d 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/Widgets/DistroDropdown.tsx +++ b/apps/spruce/src/components/Spawn/spawnHostModal/Widgets/DistroDropdown.tsx @@ -1,66 +1,53 @@ import styled from "@emotion/styled"; import { palette } from "@leafygreen-ui/palette"; import { Overline } from "@leafygreen-ui/typography"; -import SearchableDropdown, { - SearchableDropdownProps, -} from "components/SearchableDropdown"; +import SearchableDropdown from "components/SearchableDropdown"; import ElementWrapper from "components/SpruceForm/ElementWrapper"; -import { SpruceWidgetProps } from "components/SpruceForm/Widgets/types"; +import { EnumSpruceWidgetProps } from "components/SpruceForm/Widgets/types"; import { size } from "constants/tokens"; const { gray } = palette; interface DistroValue { - value: string; - isVirtualWorkstation: boolean; -} -interface OptionValue { - title: string; - distros: DistroValue[]; + adminOnly: boolean; + isVirtualWorkStation: boolean; + name: string; } interface DistroEnum { options: { - enumOptions: Array<{ - schema: { - adminOnly: boolean; - isVirtualWorkstation: boolean; - }; - label: string; - value: string; - }>; + distros: DistroValue[]; }; } -export const DistroDropdown: React.FC< - DistroEnum & - SpruceWidgetProps & { - options: Pick, "data-cy">; - } -> = ({ label, onChange, options, ...rest }) => { +export const DistroDropdown: React.FC = ({ + label, + onChange, + options, + value, +}) => { const { ariaLabelledBy, "data-cy": dataCy, + distros: distroList, elementWrapperCSS, - enumOptions, } = options; - const searchableOptions = categorizeDistros(enumOptions); - const selectedDistro = rest.value?.value; + const searchableOptions = categorizeDistros(distroList); return ( + searchFunc={(items: DistroGroup[], match: string) => items.map((e) => ({ ...e, - distros: e.distros.filter(({ value }) => - value.toLowerCase().includes(match.toLowerCase()), + distros: e.distros.filter((d: string) => + d.toLowerCase().includes(match.toLowerCase()), ), })) } @@ -77,21 +64,24 @@ export const DistroDropdown: React.FC< ); }; -// Bucketize distros into admin-only, workstation, and Non-Workstation buckets. Admin-only takes precedence over workstation. -const categorizeDistros = (distros: DistroEnum["options"]["enumOptions"]) => - distros.reduce( - (accum, { schema, value }) => { - const { adminOnly, isVirtualWorkstation } = schema; +type DistroGroup = { + title: string; + distros: string[]; +}; +// Bucketize distros into admin-only, workstation, and Non-Workstation buckets. Admin-only takes precedence over workstation. +const categorizeDistros = (distros: DistroValue[]): DistroGroup[] => + distros?.reduce( + (accum, { adminOnly, isVirtualWorkStation, name }) => { // Default to standard distro let categoryIndex = 1; if (adminOnly) { categoryIndex = 2; - } else if (isVirtualWorkstation) { + } else if (isVirtualWorkStation) { categoryIndex = 0; } - accum[categoryIndex].distros.push({ value, isVirtualWorkstation }); + accum[categoryIndex].distros.push(name); return accum; }, @@ -104,8 +94,8 @@ const categorizeDistros = (distros: DistroEnum["options"]["enumOptions"]) => const DropdownOption: React.FC<{ title: string; - distros: DistroValue[]; - onClick: (distro: DistroValue) => void; + distros: string[]; + onClick: (distro: string) => void; }> = ({ distros, onClick, title }) => distros.length > 0 ? ( @@ -114,10 +104,10 @@ const DropdownOption: React.FC<{ {distros.map((d) => ( ))} diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx b/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx index 2839ff8f3..e8e3db461 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx +++ b/apps/spruce/src/components/Spawn/spawnHostModal/getFormSchema.tsx @@ -74,24 +74,9 @@ export const getFormSchema = ({ distro: { type: "string" as "string", title: "Distro", - default: distroIdQueryParam - ? { - value: distroIdQueryParam, - isVirtualWorkstation: !!distros?.find( - (v) => - v.name === distroIdQueryParam && v.isVirtualWorkStation, - ), - } - : null, - oneOf: [ - ...(distros?.map((d) => ({ - type: "string" as "string", - title: d.name, - enum: [d.name], - isVirtualWorkstation: d.isVirtualWorkStation, - adminOnly: d.adminOnly, - })) || []), - ], + default: distroIdQueryParam, + enum: distros?.map(({ name }) => name), + minLength: 1, }, region: { type: "string" as "string", @@ -104,6 +89,7 @@ export const getFormSchema = ({ enum: [r], })) || []), ], + minLength: 1, }, publicKeySection: { title: "", @@ -141,6 +127,7 @@ export const getFormSchema = ({ default: myPublicKeys?.length ? myPublicKeys[0]?.name : "", + minLength: 1, oneOf: myPublicKeys?.length > 0 ? myPublicKeys.map((d) => ({ @@ -167,6 +154,7 @@ export const getFormSchema = ({ title: "Public key", type: "string" as "string", default: "", + minLength: 1, }, savePublicKey: { title: "Save Public Key", @@ -177,6 +165,13 @@ export const getFormSchema = ({ dependencies: { savePublicKey: { oneOf: [ + { + properties: { + savePublicKey: { + enum: [false], + }, + }, + }, { properties: { savePublicKey: { @@ -185,6 +180,8 @@ export const getFormSchema = ({ newPublicKeyName: { title: "Key name", type: "string" as "string", + default: "", + minLength: 1, }, }, }, @@ -220,6 +217,8 @@ export const getFormSchema = ({ userdataScript: { title: "Userdata Script", type: "string" as "string", + default: "", + minLength: 1, }, }, }, @@ -255,12 +254,14 @@ export const getFormSchema = ({ setupScript: { title: "Setup Script", type: "string" as "string", + default: "", + minLength: 1, }, }, }, { properties: { - runUserdataScript: { + defineSetupScriptCheckbox: { enum: [false], }, }, @@ -320,6 +321,7 @@ export const getFormSchema = ({ type: "string" as "string", title: "Expiration", default: getDefaultExpiration(), + minLength: 6, }, }, dependencies: { @@ -330,9 +332,6 @@ export const getFormSchema = ({ noExpiration: { enum: [false], }, - expiration: { - readOnly: false, - }, }, }, { @@ -384,6 +383,7 @@ export const getFormSchema = ({ title: "Volume", type: "string" as "string", default: availableVolumes[0]?.id ?? "", + minLength: 1, oneOf: availableVolumes.length > 0 ? availableVolumes.map((v) => ({ @@ -402,6 +402,7 @@ export const getFormSchema = ({ }, }, { + required: ["volumeSize"], properties: { selectExistingVolume: { enum: [false], @@ -410,6 +411,7 @@ export const getFormSchema = ({ title: "Volume size (GB)", type: "number" as "number", default: DEFAULT_VOLUME_SIZE, + minimum: 1, }, }, }, @@ -449,6 +451,7 @@ export const getFormSchema = ({ "ui:widget": DistroDropdown, "ui:elementWrapperCSS": dropdownWrapperClassName, "ui:data-cy": "distro-input", + "ui:distros": distros, }, region: { "ui:data-cy": "region-select", diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/index.ts b/apps/spruce/src/components/Spawn/spawnHostModal/index.ts index 353c15b6d..da3c20105 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/index.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/index.ts @@ -3,14 +3,12 @@ import { formToGql } from "./transformer"; import { FormState } from "./types"; import { useLoadFormSchemaData } from "./useLoadFormSchemaData"; import { useVirtualWorkstationDefaultExpiration } from "./useVirtualWorkstationDefaultExpiration"; -import { validateSpawnHostForm } from "./utils"; export { formToGql, getFormSchema, useLoadFormSchemaData, useVirtualWorkstationDefaultExpiration, - validateSpawnHostForm, }; export type { FormState }; diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/transformer.test.ts b/apps/spruce/src/components/Spawn/spawnHostModal/transformer.test.ts index 48c107518..0eea1cd99 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/transformer.test.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/transformer.test.ts @@ -2,9 +2,10 @@ import { formToGql } from "./transformer"; describe("spawn host modal", () => { it("correctly converts from a form to GQL", () => { - data.forEach(({ formData, mutationInput }) => { + data.forEach(({ formData, mutationInput }, i) => { expect( formToGql({ + isVirtualWorkStation: i === 0, formData, myPublicKeys, spawnTaskData: null, @@ -14,9 +15,10 @@ describe("spawn host modal", () => { }); it("migrate volume id should be reflected in the gql output when supplied", () => { const migrateVolumeId = "some_volume"; - data.forEach(({ formData, mutationInput }) => { + data.forEach(({ formData, mutationInput }, i) => { expect( formToGql({ + isVirtualWorkStation: i === 0, formData, myPublicKeys, spawnTaskData: null, @@ -36,11 +38,7 @@ const myPublicKeys = [{ name: "a_key", key: "key value" }]; const data = [ { formData: { - distro: { - value: "ubuntu1804-workstation", - isVirtualWorkstation: true, - adminOnly: false, - }, + distro: "ubuntu1804-workstation", region: "us-east-1", publicKeySection: { useExisting: false, @@ -91,11 +89,7 @@ const data = [ }, { formData: { - distro: { - value: "rhel71-power8-large", - isVirtualWorkstation: false, - adminOnly: false, - }, + distro: "rhel71-power8-large", region: "rofl-east", publicKeySection: { useExisting: true, diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/transformer.ts b/apps/spruce/src/components/Spawn/spawnHostModal/transformer.ts index d692fb785..9926d8f4c 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/transformer.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/transformer.ts @@ -9,6 +9,7 @@ import { FormState } from "./types"; import { validateTask } from "./utils"; interface Props { + isVirtualWorkStation: boolean; formData: FormState; myPublicKeys: MyPublicKeysQuery["myPublicKeys"]; spawnTaskData?: SpawnTaskQuery["task"]; @@ -16,6 +17,7 @@ interface Props { } export const formToGql = ({ formData, + isVirtualWorkStation, migrateVolumeId, myPublicKeys, spawnTaskData, @@ -30,7 +32,6 @@ export const formToGql = ({ setupScriptSection, userdataScriptSection, } = formData || {}; - const isVirtualWorkStation = !!distro?.isVirtualWorkstation; return { isVirtualWorkStation, userDataScript: userdataScriptSection?.runUserdataScript @@ -64,7 +65,7 @@ export const formToGql = ({ }, savePublicKey: !publicKeySection?.useExisting && !!publicKeySection?.savePublicKey, - distroId: distro?.value, + distroId: distro, region, taskId: loadData?.loadDataOntoHostAtStartup && validateTask(spawnTaskData) diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/types.ts b/apps/spruce/src/components/Spawn/spawnHostModal/types.ts index abaac68be..b517b3d70 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/types.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/types.ts @@ -1,9 +1,5 @@ export type FormState = { - distro?: { - adminOnly: boolean; - isVirtualWorkstation: boolean; - value: string; - }; + distro?: string; region?: string; publicKeySection?: { useExisting: boolean; diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/useVirtualWorkstationDefaultExpiration.ts b/apps/spruce/src/components/Spawn/spawnHostModal/useVirtualWorkstationDefaultExpiration.ts index 4f7bf7b24..2580e4af1 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/useVirtualWorkstationDefaultExpiration.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/useVirtualWorkstationDefaultExpiration.ts @@ -4,16 +4,17 @@ import { getDefaultExpiration } from "../utils"; import { FormState } from "./types"; interface Props { - setFormState: (formState: FormState) => void; - formState: FormState; disableExpirationCheckbox: boolean; + formState: FormState; + isVirtualWorkstation: boolean; + setFormState: (formState: FormState) => void; } export const useVirtualWorkstationDefaultExpiration = ({ disableExpirationCheckbox, formState, + isVirtualWorkstation, setFormState, }: Props) => { - const isVirtualWorkstation = !!formState?.distro?.isVirtualWorkstation; // Default virtual workstations to unexpirable upon selection if possible const prevIsVirtualWorkStation = usePrevious(isVirtualWorkstation); useEffect(() => { diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/utils.test.ts b/apps/spruce/src/components/Spawn/spawnHostModal/utils.test.ts deleted file mode 100644 index 8bab5dcea..000000000 --- a/apps/spruce/src/components/Spawn/spawnHostModal/utils.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { FormState } from "./types"; -import { validateSpawnHostForm } from "./utils"; - -describe("validateSpawnHostForm", () => { - it("a valid form will return true", () => { - expect(validateSpawnHostForm(validForm)).toBe(true); - }); - it("an empty form will return false", () => { - expect(validateSpawnHostForm({})).toBe(false); - }); - it("a home volume name or size must be provided after selecting a virtual workstation distro when not migrating a volume", () => { - expect(validateSpawnHostForm(validVirtualWorkstationForm)).toBe(true); - expect( - validateSpawnHostForm({ - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: true, volumeSelect: "" }, - }), - ).toBe(false); - expect( - validateSpawnHostForm({ - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: false, volumeSize: 0 }, - }), - ).toBe(false); - expect( - validateSpawnHostForm({ - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: false, volumeSize: 1 }, - }), - ).toBe(true); - }); - it("home volume inputs are not required when migrating a volume", () => { - expect( - validateSpawnHostForm( - { - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: true, volumeSelect: "" }, - }, - true, - ), - ).toBe(true); - expect( - validateSpawnHostForm( - { - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: false, volumeSize: 0 }, - }, - true, - ), - ).toBe(true); - expect( - validateSpawnHostForm( - { - ...validVirtualWorkstationForm, - homeVolumeDetails: { selectExistingVolume: false, volumeSize: 1 }, - }, - true, - ), - ).toBe(true); - }); - it("an empty distro will not pass validation", () => { - expect( - validateSpawnHostForm({ - ...validForm, - distro: { value: "", isVirtualWorkstation: false, adminOnly: false }, - }), - ).toBe(false); - }); - it("an empty region will not pass validation", () => { - expect( - validateSpawnHostForm({ - ...validForm, - region: "", - }), - ).toBe(false); - }); - it("a public key name or public key value must be provided", () => { - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { useExisting: true, publicKeyNameDropdown: "" }, - }), - ).toBe(false); - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { useExisting: false, newPublicKey: "" }, - }), - ).toBe(false); - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { - useExisting: true, - publicKeyNameDropdown: "key val", - }, - }), - ).toBe(true); - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { useExisting: false, newPublicKey: "key val" }, - }), - ).toBe(true); - }); - it("a public key name is required when 'Save Public Key' is checked", () => { - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { - useExisting: false, - newPublicKey: "ssh-rsa new-key", - savePublicKey: true, - newPublicKeyName: "new key", - }, - }), - ).toBe(true); - expect( - validateSpawnHostForm({ - ...validForm, - publicKeySection: { - useExisting: false, - newPublicKey: "ssh-rsa new-key", - savePublicKey: true, - }, - }), - ).toBe(false); - }); - it("a user data script must be provided when the option is selected", () => { - expect( - validateSpawnHostForm({ - ...validForm, - userdataScriptSection: { - runUserdataScript: true, - userdataScript: "", - }, - }), - ).toBe(false); - expect( - validateSpawnHostForm({ - ...validForm, - userdataScriptSection: { - runUserdataScript: true, - userdataScript: "abc", - }, - }), - ).toBe(true); - }); - it("a setup script must be provided when the option is selected", () => { - expect( - validateSpawnHostForm({ - ...validForm, - setupScriptSection: { - defineSetupScriptCheckbox: true, - setupScript: "abc123", - }, - }), - ).toBe(true); - expect( - validateSpawnHostForm({ - ...validForm, - setupScriptSection: { - defineSetupScriptCheckbox: true, - setupScript: "", - }, - }), - ).toBe(false); - }); - it("an expiration is required when 'Never expire' is not selected", () => { - expect( - validateSpawnHostForm({ - ...validForm, - expirationDetails: { - noExpiration: false, - expiration: "2022-10-11T22:08:02.000Z", - }, - }), - ).toBe(true); - expect( - validateSpawnHostForm({ - ...validForm, - expirationDetails: { - noExpiration: false, - expiration: "", - }, - }), - ).toBe(false); - }); -}); - -const validForm: FormState = { - distro: { - value: "ubuntu-workstation", - isVirtualWorkstation: true, - adminOnly: false, - }, - region: "us-east-1a", - publicKeySection: { - useExisting: true, - publicKeyNameDropdown: "cool key", - }, - homeVolumeDetails: { - selectExistingVolume: true, - volumeSelect: "a volume", - }, - expirationDetails: { - noExpiration: true, - }, -}; -const validVirtualWorkstationForm: FormState = { - distro: { - value: "ubuntu-workstation", - isVirtualWorkstation: true, - adminOnly: false, - }, - region: "us-east-1a", - publicKeySection: { - useExisting: true, - publicKeyNameDropdown: "cool key", - }, - homeVolumeDetails: { - selectExistingVolume: true, - volumeSelect: "a volume", - }, - expirationDetails: { - noExpiration: true, - }, -}; diff --git a/apps/spruce/src/components/Spawn/spawnHostModal/utils.ts b/apps/spruce/src/components/Spawn/spawnHostModal/utils.ts index 0c99bc030..c0c3b7482 100644 --- a/apps/spruce/src/components/Spawn/spawnHostModal/utils.ts +++ b/apps/spruce/src/components/Spawn/spawnHostModal/utils.ts @@ -1,5 +1,4 @@ import { SpawnTaskQuery } from "gql/generated/types"; -import { FormState } from "./types"; export const validateTask = (taskData: SpawnTaskQuery["task"]) => { const { @@ -9,50 +8,3 @@ export const validateTask = (taskData: SpawnTaskQuery["task"]) => { } = taskData || {}; return taskDisplayName && buildVariant && revision; }; - -export const validateSpawnHostForm = ( - { - distro, - expirationDetails, - homeVolumeDetails, - publicKeySection, - region, - setupScriptSection, - userdataScriptSection, - }: FormState, - isMigration?: boolean, -) => { - const hasDistro = !!distro?.value; - const hasRegion = !!region; - const hasPublicKey = publicKeySection?.useExisting - ? !!publicKeySection?.publicKeyNameDropdown - : !!publicKeySection?.newPublicKey; - const hasValidPublicKeyName = publicKeySection?.savePublicKey - ? !!publicKeySection?.newPublicKeyName - : true; - const hasValidUserdataScript = userdataScriptSection?.runUserdataScript - ? !!userdataScriptSection?.userdataScript - : true; - const hasValidSetupScript = setupScriptSection?.defineSetupScriptCheckbox - ? !!setupScriptSection?.setupScript - : true; - const hasValidHomeVolumeDetails = - isMigration || - (homeVolumeDetails?.selectExistingVolume - ? !!homeVolumeDetails?.volumeSelect - : !!homeVolumeDetails?.volumeSize); - const hasValidExpiration = expirationDetails?.noExpiration - ? true - : !!expirationDetails?.expiration; - - return ( - hasDistro && - hasRegion && - hasPublicKey && - hasValidPublicKeyName && - hasValidUserdataScript && - hasValidSetupScript && - (distro?.isVirtualWorkstation ? hasValidHomeVolumeDetails : true) && - hasValidExpiration - ); -}; diff --git a/apps/spruce/src/pages/spawn/spawnHost/spawnHostButton/SpawnHostModal.tsx b/apps/spruce/src/pages/spawn/spawnHost/spawnHostButton/SpawnHostModal.tsx index 48a5e7978..a8fd47fe2 100644 --- a/apps/spruce/src/pages/spawn/spawnHost/spawnHostButton/SpawnHostModal.tsx +++ b/apps/spruce/src/pages/spawn/spawnHost/spawnHostButton/SpawnHostModal.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useQuery, useMutation } from "@apollo/client"; import { useLocation } from "react-router-dom"; import { useSpawnAnalytics } from "analytics"; @@ -8,7 +8,6 @@ import { getFormSchema, useLoadFormSchemaData, useVirtualWorkstationDefaultExpiration, - validateSpawnHostForm, FormState, } from "components/Spawn/spawnHostModal"; import { SpruceForm } from "components/SpruceForm"; @@ -69,8 +68,16 @@ export const SpawnHostModal: React.FC = ({ }); const [formState, setFormState] = useState({}); + const [hasError, setHasError] = useState(true); + + const selectedDistro = useMemo( + () => + formSchemaInput?.distros?.find(({ name }) => name === formState.distro), + [formSchemaInput.distros, formState.distro], + ); useVirtualWorkstationDefaultExpiration({ + isVirtualWorkstation: selectedDistro?.isVirtualWorkStation, setFormState, formState, disableExpirationCheckbox: formSchemaInput.disableExpirationCheckbox, @@ -80,7 +87,7 @@ export const SpawnHostModal: React.FC = ({ ...formSchemaInput, distroIdQueryParam, isMigration: false, - isVirtualWorkstation: !!formState?.distro?.isVirtualWorkstation, + isVirtualWorkstation: !!selectedDistro?.isVirtualWorkStation, spawnTaskData: spawnTaskData?.task, useSetupScript: !!formState?.setupScriptSection?.defineSetupScriptCheckbox, useProjectSetupScript: !!formState?.loadData?.runProjectSpecificSetupScript, @@ -92,6 +99,7 @@ export const SpawnHostModal: React.FC = ({ const spawnHost = () => { const mutationInput = formToGql({ + isVirtualWorkStation: selectedDistro?.isVirtualWorkStation, formData: formState, myPublicKeys: formSchemaInput.myPublicKeys, spawnTaskData: spawnTaskData?.task, @@ -115,9 +123,7 @@ export const SpawnHostModal: React.FC = ({ title="Spawn New Host" open={open} data-cy="spawn-host-modal" - submitDisabled={ - !validateSpawnHostForm(formState, false) || loadingSpawnHost - } + submitDisabled={hasError || loadingSpawnHost} onCancel={() => { setOpen(false); }} @@ -128,8 +134,9 @@ export const SpawnHostModal: React.FC = ({ schema={schema} uiSchema={uiSchema} formData={formState} - onChange={({ formData }) => { + onChange={({ errors, formData }) => { setFormState(formData); + setHasError(errors.length > 0); }} /> diff --git a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx index 216bdd297..6217c8b6a 100644 --- a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx +++ b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/MigrateVolumeModal.tsx @@ -8,7 +8,6 @@ import { getFormSchema, useLoadFormSchemaData, useVirtualWorkstationDefaultExpiration, - validateSpawnHostForm, } from "components/Spawn/spawnHostModal"; import { SpruceForm } from "components/SpruceForm"; import { useToastContext } from "context/toast"; @@ -33,7 +32,10 @@ export const MigrateVolumeModal: React.FC = ({ setOpen, volume, }) => { - const [{ form, page }, dispatch] = useReducer(reducer, initialState); + const [{ form, hasError, page }, dispatch] = useReducer( + reducer, + initialState, + ); const onPageOne = page === Page.First; const dispatchToast = useToastContext(); @@ -65,14 +67,21 @@ export const MigrateVolumeModal: React.FC = ({ () => formSchemaInput.distros?.filter((d) => d.isVirtualWorkStation), [formSchemaInput.distros], ); + + const selectedDistro = useMemo( + () => distros?.find(({ name }) => name === form?.distro), + [distros, form.distro], + ); + const { schema, uiSchema } = getFormSchema({ ...formSchemaInput, distros, isMigration: true, - isVirtualWorkstation: !!form?.distro?.isVirtualWorkstation, + isVirtualWorkstation: !!selectedDistro?.isVirtualWorkStation, userAwsRegion: AZToRegion(volume.availabilityZone), }); useVirtualWorkstationDefaultExpiration({ + isVirtualWorkstation: selectedDistro?.isVirtualWorkStation, disableExpirationCheckbox: formSchemaInput.disableExpirationCheckbox, formState: form, setFormState: (formState) => @@ -87,6 +96,7 @@ export const MigrateVolumeModal: React.FC = ({ const migrateVolume = useCallback(() => { const mutationInput = formToGql({ + isVirtualWorkStation: !!selectedDistro?.isVirtualWorkStation, formData: form, myPublicKeys: formSchemaInput.myPublicKeys, migrateVolumeId: volume.id, @@ -112,6 +122,7 @@ export const MigrateVolumeModal: React.FC = ({ volume, migrateVolumeMutation, sendEvent, + selectedDistro?.isVirtualWorkStation, ]); const title = onPageOne @@ -148,11 +159,7 @@ export const MigrateVolumeModal: React.FC = ({ = ({ schema={schema} uiSchema={uiSchema} formData={form} - onChange={({ formData }) => { + onChange={({ errors, formData }) => { dispatch({ type: "setForm", payload: formData }); + dispatch({ type: "setHasError", payload: errors.length > 0 }); }} /> )} diff --git a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/migrateVolumeReducer.ts b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/migrateVolumeReducer.ts index 278d0bdab..a2e5bb5af 100644 --- a/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/migrateVolumeReducer.ts +++ b/apps/spruce/src/pages/spawn/spawnVolume/spawnVolumeTableActions/migrateVolumeReducer.ts @@ -6,11 +6,12 @@ export enum Page { } interface State { + hasError: boolean; page: Page; form: FormState; } -export const initialState = { page: Page.First, form: {} }; +export const initialState = { page: Page.First, form: {}, hasError: true }; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -23,6 +24,8 @@ export const reducer = (state: State, action: Action): State => { return { ...state, page: Page.First }; case "setForm": return { ...state, form: action.payload }; + case "setHasError": + return { ...state, hasError: action.payload }; case "resetForm": return { ...state, form: {} }; default: @@ -34,4 +37,5 @@ type Action = | { type: "goToNextPage" } | { type: "resetForm" } | { type: "resetPage" } + | { type: "setHasError"; payload: boolean } | { type: "setForm"; payload: FormState }; diff --git a/package.json b/package.json index a47f7a885..f9305a256 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "scripts": { "prepare": "husky", - "test": "jest" + "test": "jest --watchAll=false" }, "resolutions": { "@tanstack/react-table": "8.9.3",