From 59629b0b3a18b5e204a88232628e9d67c6048a9a Mon Sep 17 00:00:00 2001 From: Sophie Stadler Date: Fri, 8 Sep 2023 11:18:44 -0400 Subject: [PATCH] EVG-19950: Add host settings page (#2014) --- .../distroSettings/host_section.ts | 60 ++++ src/components/SpruceForm/ElementWrapper.tsx | 10 +- .../SpruceForm/FieldTemplates/index.tsx | 1 + .../SpruceForm/Widgets/LeafyGreenWidgets.tsx | 105 +++---- .../SpruceForm/Widgets/MultiSelect.tsx | 43 ++- src/gql/generated/types.ts | 1 + src/gql/mocks/getSpruceConfig.ts | 6 + src/gql/queries/get-spruce-config.graphql | 4 + src/pages/distroSettings/HeaderButtons.tsx | 4 +- src/pages/distroSettings/Tabs.tsx | 18 +- .../distroSettings/tabs/HostTab/HostTab.tsx | 17 ++ .../tabs/HostTab/getFormSchema.tsx | 270 ++++++++++++++++++ .../tabs/HostTab/transformers.test.ts | 73 +++++ .../tabs/HostTab/transformers.ts | 72 +++++ .../distroSettings/tabs/HostTab/types.ts | 42 +++ src/pages/distroSettings/tabs/index.tsx | 3 +- src/pages/distroSettings/tabs/transformers.ts | 9 +- src/pages/distroSettings/tabs/types.ts | 4 +- .../tabs/VariablesTab/VariableRow.tsx | 1 + 19 files changed, 645 insertions(+), 98 deletions(-) create mode 100644 cypress/integration/distroSettings/host_section.ts create mode 100644 src/pages/distroSettings/tabs/HostTab/HostTab.tsx create mode 100644 src/pages/distroSettings/tabs/HostTab/getFormSchema.tsx create mode 100644 src/pages/distroSettings/tabs/HostTab/transformers.test.ts create mode 100644 src/pages/distroSettings/tabs/HostTab/transformers.ts create mode 100644 src/pages/distroSettings/tabs/HostTab/types.ts diff --git a/cypress/integration/distroSettings/host_section.ts b/cypress/integration/distroSettings/host_section.ts new file mode 100644 index 0000000000..cc7958243f --- /dev/null +++ b/cypress/integration/distroSettings/host_section.ts @@ -0,0 +1,60 @@ +import { save } from "./utils"; + +describe("host section", () => { + describe("using legacy ssh", () => { + beforeEach(() => { + cy.visit("/distro/localhost/settings/host"); + }); + + it("shows the correct fields when distro has static provider", () => { + cy.dataCy("authorized-keys-input").should("exist"); + cy.dataCy("minimum-hosts-input").should("not.exist"); + cy.dataCy("maximum-hosts-input").should("not.exist"); + cy.dataCy("idle-time-input").should("not.exist"); + cy.dataCy("future-fraction-input").should("not.exist"); + }); + + it("errors when selecting an incompatible host communication method", () => { + cy.selectLGOption("Host Communication Method", "RPC"); + save(); + cy.validateToast( + "error", + "validating changes for distro 'localhost': 'ERROR: bootstrapping hosts using legacy SSH is incompatible with non-legacy host communication'" + ); + cy.selectLGOption("Host Communication Method", "Legacy SSH"); + }); + + it("updates host fields", () => { + cy.selectLGOption("Agent Architecture", "Linux ARM 64-bit"); + cy.getInputByLabel("Working Directory").clear(); + cy.getInputByLabel("Working Directory").type("/usr/local/bin"); + cy.getInputByLabel("SSH User").clear(); + cy.getInputByLabel("SSH User").type("sudo"); + cy.contains("button", "Add SSH option").click(); + cy.getInputByLabel("SSH Option").type("BatchMode=yes"); + cy.selectLGOption("Host Allocator Rounding Rule", "Round down"); + cy.selectLGOption("Host Allocator Feedback Rule", "No feedback"); + cy.selectLGOption( + "Host Overallocation Rule", + "Terminate hosts when overallocated" + ); + + save(); + cy.validateToast("success"); + + // Reset fields + cy.selectLGOption("Agent Architecture", "Linux 64-bit"); + cy.getInputByLabel("Working Directory").clear(); + cy.getInputByLabel("Working Directory").type("/home/ubuntu/smoke"); + cy.getInputByLabel("SSH User").clear(); + cy.getInputByLabel("SSH User").type("ubuntu"); + cy.dataCy("delete-item-button").click(); + cy.selectLGOption("Host Allocator Rounding Rule", "Default"); + cy.selectLGOption("Host Allocator Feedback Rule", "Default"); + cy.selectLGOption("Host Overallocation Rule", "Default"); + + save(); + cy.validateToast("success"); + }); + }); +}); diff --git a/src/components/SpruceForm/ElementWrapper.tsx b/src/components/SpruceForm/ElementWrapper.tsx index 406631e0e7..aa5757e618 100644 --- a/src/components/SpruceForm/ElementWrapper.tsx +++ b/src/components/SpruceForm/ElementWrapper.tsx @@ -1,7 +1,15 @@ import styled from "@emotion/styled"; +import { STANDARD_FIELD_WIDTH } from "./utils"; -const ElementWrapper = styled.div` +type ElementWrapperProps = { + limitMaxWidth?: boolean; +}; + +const ElementWrapper = styled.div` margin-bottom: 20px; + + ${({ limitMaxWidth }) => + limitMaxWidth && `max-width: ${STANDARD_FIELD_WIDTH}px;`} `; export default ElementWrapper; diff --git a/src/components/SpruceForm/FieldTemplates/index.tsx b/src/components/SpruceForm/FieldTemplates/index.tsx index 394a5ea2ae..18cc578f4b 100644 --- a/src/components/SpruceForm/FieldTemplates/index.tsx +++ b/src/components/SpruceForm/FieldTemplates/index.tsx @@ -50,4 +50,5 @@ const DefaultFieldContainer = styled.div<{ border?: "top" | "bottom" }>` ${({ border }) => border && `border-${border}: 1px solid ${gray.light1}; padding-${border}: ${size.s};`} + width: 100%; `; diff --git a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx index 1d080e1639..3be16b0ff6 100644 --- a/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx +++ b/src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx @@ -19,7 +19,6 @@ import Icon from "components/Icon"; import { size, zIndex } from "constants/tokens"; import { OneOf } from "types/utils"; import ElementWrapper from "../ElementWrapper"; -import { STANDARD_FIELD_WIDTH } from "../utils"; import { EnumSpruceWidgetProps, SpruceWidgetProps } from "./types"; import { isNullish, processErrors } from "./utils"; @@ -59,30 +58,26 @@ export const LeafyGreenTextInput: React.FC< }; return ( - - - - target.value === "" ? onChange(emptyValue) : onChange(target.value) - } - aria-label={label} - {...inputProps} - /> - {!!warnings?.length && ( - - {warnings.join(", ")} - - )} - + + + target.value === "" ? onChange(emptyValue) : onChange(target.value) + } + aria-label={label} + {...inputProps} + /> + {!!warnings?.length && ( + {warnings.join(", ")} + )} ); }; @@ -183,35 +178,33 @@ export const LeafyGreenSelect: React.FC< ariaLabelledBy ? { "aria-labelledby": ariaLabelledBy } : { label }; return ( - - - - + + ); }; @@ -433,7 +426,3 @@ const StyledSegmentedControl = styled(SegmentedControl)` box-sizing: border-box; margin-bottom: ${size.s}; `; - -export const MaxWidthContainer = styled.div` - max-width: ${STANDARD_FIELD_WIDTH}px; -`; diff --git a/src/components/SpruceForm/Widgets/MultiSelect.tsx b/src/components/SpruceForm/Widgets/MultiSelect.tsx index 6338ca78da..ad4c1a5ac0 100644 --- a/src/components/SpruceForm/Widgets/MultiSelect.tsx +++ b/src/components/SpruceForm/Widgets/MultiSelect.tsx @@ -4,7 +4,6 @@ import Dropdown from "components/Dropdown"; import { TreeSelect, ALL_VALUE } from "components/TreeSelect"; import { size } from "constants/tokens"; import ElementWrapper from "../ElementWrapper"; -import { MaxWidthContainer } from "./LeafyGreenWidgets"; import { EnumSpruceWidgetProps } from "./types"; export const MultiSelect: React.FC = ({ @@ -39,28 +38,26 @@ export const MultiSelect: React.FC = ({ const selectedOptions = [...value, ...(includeAll ? [ALL_VALUE] : [])]; return ( - - - - - - - - {rawErrors.length > 0 && {rawErrors.join(", ")}} - - + + + + + + + {rawErrors.length > 0 && {rawErrors.join(", ")}} + ); }; diff --git a/src/gql/generated/types.ts b/src/gql/generated/types.ts index 3724f371d2..59f47f7dd8 100644 --- a/src/gql/generated/types.ts +++ b/src/gql/generated/types.ts @@ -7809,6 +7809,7 @@ export type SpruceConfigQuery = { banner?: string | null; bannerTheme?: string | null; jira?: { __typename?: "JiraConfig"; host?: string | null } | null; + keys: Array<{ __typename?: "SSHKey"; location: string; name: string }>; providers?: { __typename?: "CloudProviderConfig"; aws?: { diff --git a/src/gql/mocks/getSpruceConfig.ts b/src/gql/mocks/getSpruceConfig.ts index 2fc31490b0..5061ee8cc2 100644 --- a/src/gql/mocks/getSpruceConfig.ts +++ b/src/gql/mocks/getSpruceConfig.ts @@ -22,6 +22,12 @@ export const getSpruceConfigMock: ApolloMock< defaultProject: "evergreen", __typename: "UIConfig", }, + keys: [ + { + name: "fake_key", + location: "/path/to/key", + }, + ], jira: { host: "jira.mongodb.org", __typename: "JiraConfig" }, providers: { aws: { diff --git a/src/gql/queries/get-spruce-config.graphql b/src/gql/queries/get-spruce-config.graphql index a89b2e2fd6..aebb318891 100644 --- a/src/gql/queries/get-spruce-config.graphql +++ b/src/gql/queries/get-spruce-config.graphql @@ -5,6 +5,10 @@ query SpruceConfig { jira { host } + keys { + location + name + } providers { aws { maxVolumeSizePerUser diff --git a/src/pages/distroSettings/HeaderButtons.tsx b/src/pages/distroSettings/HeaderButtons.tsx index 2d8137b8b3..4818fbf476 100644 --- a/src/pages/distroSettings/HeaderButtons.tsx +++ b/src/pages/distroSettings/HeaderButtons.tsx @@ -18,7 +18,7 @@ import { import { SAVE_DISTRO } from "gql/mutations"; import { useDistroSettingsContext } from "./Context"; import { formToGqlMap } from "./tabs/transformers"; -import { WritableDistroSettingsType } from "./tabs/types"; +import { FormToGqlFunction, WritableDistroSettingsType } from "./tabs/types"; interface Props { distro: DistroQuery["distro"]; @@ -64,7 +64,7 @@ export const HeaderButtons: React.FC = ({ distro, tab }) => { // Only perform the save operation is the tab is valid. // eslint-disable-next-line no-prototype-builtins if (formToGqlMap.hasOwnProperty(tab)) { - const formToGql = formToGqlMap[tab]; + const formToGql: FormToGqlFunction = formToGqlMap[tab]; const changes = formToGql(formData, distro); saveDistro({ variables: { diff --git a/src/pages/distroSettings/Tabs.tsx b/src/pages/distroSettings/Tabs.tsx index 5393a0f00d..65e5a374af 100644 --- a/src/pages/distroSettings/Tabs.tsx +++ b/src/pages/distroSettings/Tabs.tsx @@ -9,11 +9,13 @@ import { NavigationModal } from "./NavigationModal"; import { EventLogTab, GeneralTab, + HostTab, ProjectTab, ProviderTab, TaskTab, } from "./tabs/index"; import { gqlToFormMap } from "./tabs/transformers"; +import { FormStateMap } from "./tabs/types"; interface Props { distro: DistroQuery["distro"]; @@ -26,7 +28,6 @@ export const DistroSettingsTabs: React.FC = ({ distro }) => { const tabData = useMemo(() => getTabData(distro), [distro]); useEffect(() => { - // @ts-expect-error TODO: Type when all tabs have been implemented setInitialData(tabData); }, [setInitialData, tabData]); @@ -59,6 +60,15 @@ export const DistroSettingsTabs: React.FC = ({ distro }) => { } /> + + } + /> = ({ distro }) => { ); }; -/* Map data from query to the tab to which it will be passed */ -// TODO: Type when all tabs have been implemented -const getTabData = (data: Props["distro"]) => +const getTabData = (data: Props["distro"]): FormStateMap => Object.keys(gqlToFormMap).reduce( (obj, tab) => ({ ...obj, [tab]: gqlToFormMap[tab](data), }), - {} + {} as FormStateMap ); const Container = styled.div` diff --git a/src/pages/distroSettings/tabs/HostTab/HostTab.tsx b/src/pages/distroSettings/tabs/HostTab/HostTab.tsx new file mode 100644 index 0000000000..f7df1db7db --- /dev/null +++ b/src/pages/distroSettings/tabs/HostTab/HostTab.tsx @@ -0,0 +1,17 @@ +import { useMemo } from "react"; +import { useSpruceConfig } from "hooks"; +import { BaseTab } from "../BaseTab"; +import { getFormSchema } from "./getFormSchema"; +import { TabProps } from "./types"; + +export const HostTab: React.FC = ({ distroData, provider }) => { + const spruceConfig = useSpruceConfig(); + const sshKeys = spruceConfig?.keys; + + const formSchema = useMemo( + () => getFormSchema({ provider, sshKeys }), + [provider, sshKeys] + ); + + return ; +}; diff --git a/src/pages/distroSettings/tabs/HostTab/getFormSchema.tsx b/src/pages/distroSettings/tabs/HostTab/getFormSchema.tsx new file mode 100644 index 0000000000..d284dc5415 --- /dev/null +++ b/src/pages/distroSettings/tabs/HostTab/getFormSchema.tsx @@ -0,0 +1,270 @@ +import { InlineCode } from "@leafygreen-ui/typography"; +import { GetFormSchema } from "components/SpruceForm"; +import { CardFieldTemplate } from "components/SpruceForm/FieldTemplates"; +import { + Arch, + BootstrapMethod, + CommunicationMethod, + FeedbackRule, + HostAllocatorVersion, + OverallocatedRule, + Provider, + RoundingRule, + SshKey, +} from "gql/generated/types"; + +type FormSchemaParams = { + provider: Provider; + sshKeys: SshKey[]; +}; + +export const getFormSchema = ({ + provider, + sshKeys, +}: FormSchemaParams): ReturnType => { + const hasStaticProvider = provider === Provider.Static; + const hasDockerProvider = provider === Provider.Docker; + const hasEC2Provider = !hasStaticProvider && !hasDockerProvider; + + return { + fields: {}, + schema: { + type: "object" as "object", + properties: { + setup: { + type: "object" as "object", + title: "Host Setup", + properties: { + bootstrapMethod: { + type: "string" as "string", + title: "Host Bootstrap Method", + oneOf: enumSelect(bootstrapMethodToCopy), + }, + communicationMethod: { + type: "string" as "string", + title: "Host Communication Method", + oneOf: enumSelect(communicationMethodToCopy), + }, + arch: { + type: "string" as "string", + title: "Agent Architecture", + oneOf: enumSelect(architectureToCopy), + }, + workDir: { + type: "string" as "string", + title: "Working Directory", + }, + setupScript: { + type: "string" as "string", + title: "Setup Script", + }, + setupAsSudo: { + type: "boolean" as "boolean", + title: "Run script as sudo", + }, + }, + }, + sshConfig: { + type: "object" as "object", + title: "SSH Configuration", + properties: { + user: { + type: "string" as "string", + title: "SSH User", + }, + sshKey: { + type: "string" as "string", + title: "SSH Key", + oneOf: sshKeys.map(({ location, name }) => ({ + type: "string" as "string", + title: `${name} – ${location}`, + enum: [name], + })), + }, + authorizedKeysFile: { + type: "string" as "string", + title: "Authorized Keys File", + }, + sshOptions: { + type: "array" as "array", + title: "SSH Options", + items: { + type: "string" as "string", + title: "SSH Option", + default: "", + minLength: 1, + }, + }, + }, + }, + allocation: { + type: "object" as "object", + title: "Host Allocation", + properties: { + version: { + type: "string" as "string", + title: "Host Allocator Version", + oneOf: enumSelect(hostAllocatorVersionToCopy), + }, + roundingRule: { + type: "string" as "string", + title: "Host Allocator Rounding Rule", + oneOf: enumSelect(roundingRuleToCopy), + }, + feedbackRule: { + type: "string" as "string", + title: "Host Allocator Feedback Rule", + oneOf: enumSelect(feedbackRuleToCopy), + }, + hostsOverallocatedRule: { + type: "string" as "string", + title: "Host Overallocation Rule", + oneOf: enumSelect(overallocatedRuleToCopy), + }, + minimumHosts: { + type: "number" as "number", + title: "Minimum Number of Hosts Allowed", + minimum: 0, + }, + maximumHosts: { + type: "number" as "number", + title: "Maxiumum Number of Hosts Allowed", + minimum: 0, + }, + acceptableHostIdleTime: { + type: "number" as "number", + title: "Acceptable Host Idle Time (s)", + }, + futureHostFraction: { + type: "number" as "number", + title: "Future Host Fraction", + minimum: 0, + maximum: 1, + }, + }, + }, + }, + }, + uiSchema: { + setup: { + "ui:ObjectFieldTemplate": CardFieldTemplate, + bootstrapMethod: { + "ui:allowDeselect": false, + }, + communicationMethod: { + "ui:allowDeselect": false, + }, + arch: { + "ui:allowDeselect": false, + }, + setupScript: { + "ui:widget": "textarea", + }, + }, + sshConfig: { + "ui:ObjectFieldTemplate": CardFieldTemplate, + sshKey: { + "ui:allowDeselect": false, + }, + authorizedKeysFile: { + "ui:data-cy": "authorized-keys-input", + ...(!hasStaticProvider && { "ui:widget": "hidden" }), + }, + sshOptions: { + "ui:addButtonText": "Add SSH option", + "ui:descriptionNode": ( + <> + Option keywords supported by ssh_config. + + ), + "ui:orderable": false, + items: { + "ui:placeholder": "ConnectTimeout=10", + }, + }, + }, + allocation: { + "ui:ObjectFieldTemplate": CardFieldTemplate, + version: { + "ui:allowDeselect": false, + }, + roundingRule: { + "ui:allowDeselect": false, + }, + feedbackRule: { + "ui:allowDeselect": false, + }, + hostsOverallocatedRule: { + "ui:allowDeselect": false, + }, + minimumHosts: { + "ui:data-cy": "minimum-hosts-input", + ...(!hasEC2Provider && { "ui:widget": "hidden" }), + }, + maximumHosts: { + "ui:data-cy": "maximum-hosts-input", + ...(!hasEC2Provider && { "ui:widget": "hidden" }), + }, + acceptableHostIdleTime: { + "ui:data-cy": "idle-time-input", + ...(!hasEC2Provider && { "ui:widget": "hidden" }), + }, + futureHostFraction: { + "ui:data-cy": "future-fraction-input", + ...(!hasEC2Provider && { "ui:widget": "hidden" }), + }, + }, + }, + }; +}; + +const enumSelect = (enumObject: Record) => + Object.entries(enumObject).map(([key, title]) => ({ + type: "string" as "string", + title, + enum: [key], + })); + +const architectureToCopy = { + [Arch.Linux_64Bit]: "Linux 64-bit", + [Arch.LinuxArm_64Bit]: "Linux ARM 64-bit", + [Arch.LinuxPpc_64Bit]: "Linux PowerPC 64-bit", + [Arch.LinuxZseries]: "Linux zSeries", + [Arch.Osx_64Bit]: "macOS 64-bit", + [Arch.OsxArm_64Bit]: "macOS ARM 64-bit", + [Arch.Windows_64Bit]: "Windows 64-bit", +}; + +const bootstrapMethodToCopy = { + [BootstrapMethod.LegacySsh]: "Legacy SSH", + [BootstrapMethod.Ssh]: "SSH", + [BootstrapMethod.UserData]: "User Data", +}; + +const communicationMethodToCopy = { + [CommunicationMethod.LegacySsh]: "Legacy SSH", + [CommunicationMethod.Ssh]: "SSH", + [CommunicationMethod.Rpc]: "RPC", +}; + +const hostAllocatorVersionToCopy = { + [HostAllocatorVersion.Utilization]: "Utilization", +}; + +const roundingRuleToCopy = { + [RoundingRule.Default]: "Default", + [RoundingRule.Down]: "Round down", + [RoundingRule.Up]: "Round up", +}; + +const feedbackRuleToCopy = { + [FeedbackRule.Default]: "Default", + [FeedbackRule.NoFeedback]: "No feedback", + [FeedbackRule.WaitsOverThresh]: "Wait over threshold", +}; + +const overallocatedRuleToCopy = { + [OverallocatedRule.Default]: "Default", + [OverallocatedRule.Ignore]: "No terminations when overallocated", + [OverallocatedRule.Terminate]: "Terminate hosts when overallocated", +}; diff --git a/src/pages/distroSettings/tabs/HostTab/transformers.test.ts b/src/pages/distroSettings/tabs/HostTab/transformers.test.ts new file mode 100644 index 0000000000..3f48478ef4 --- /dev/null +++ b/src/pages/distroSettings/tabs/HostTab/transformers.test.ts @@ -0,0 +1,73 @@ +import { + Arch, + BootstrapMethod, + CommunicationMethod, + DistroInput, + FeedbackRule, + HostAllocatorVersion, + OverallocatedRule, + RoundingRule, +} from "gql/generated/types"; +import { distroData } from "../testData"; +import { formToGql, gqlToForm } from "./transformers"; +import { HostFormState } from "./types"; + +describe("host tab", () => { + it("correctly converts from GQL to a form", () => { + expect(gqlToForm(distroData)).toStrictEqual(form); + }); + + it("correctly converts from a form to GQL", () => { + expect(formToGql(form, distroData)).toStrictEqual(gql); + }); +}); + +const form: HostFormState = { + setup: { + bootstrapMethod: BootstrapMethod.LegacySsh, + communicationMethod: CommunicationMethod.LegacySsh, + arch: Arch.Linux_64Bit, + workDir: "/data/evg", + setupScript: "ls -alF", + setupAsSudo: true, + }, + sshConfig: { + user: "admin", + sshKey: "fakeSshKey", + authorizedKeysFile: "", + sshOptions: ["BatchMode=yes", "ConnectTimeout=10"], + }, + allocation: { + version: HostAllocatorVersion.Utilization, + roundingRule: RoundingRule.Default, + feedbackRule: FeedbackRule.Default, + hostsOverallocatedRule: OverallocatedRule.Default, + minimumHosts: 0, + maximumHosts: 0, + acceptableHostIdleTime: 0, + futureHostFraction: 0, + }, +}; + +const gql: DistroInput = { + ...distroData, + bootstrapSettings: { + clientDir: "", + communication: CommunicationMethod.LegacySsh, + env: [], + jasperBinaryDir: "", + jasperCredentialsPath: "", + method: BootstrapMethod.LegacySsh, + preconditionScripts: [], + resourceLimits: { + lockedMemoryKb: 0, + numFiles: 0, + numProcesses: 0, + numTasks: 0, + virtualMemoryKb: 0, + }, + rootDir: "", + serviceUser: "", + shellPath: "", + }, +}; diff --git a/src/pages/distroSettings/tabs/HostTab/transformers.ts b/src/pages/distroSettings/tabs/HostTab/transformers.ts new file mode 100644 index 0000000000..9356046667 --- /dev/null +++ b/src/pages/distroSettings/tabs/HostTab/transformers.ts @@ -0,0 +1,72 @@ +import { DistroSettingsTabRoutes } from "constants/routes"; +import { FormToGqlFunction, GqlToFormFunction } from "../types"; + +type Tab = DistroSettingsTabRoutes.Host; + +export const gqlToForm = ((data) => { + if (!data) return null; + + const { + arch, + authorizedKeysFile, + bootstrapSettings: { communication, method }, + hostAllocatorSettings, + setup, + setupAsSudo, + sshKey, + sshOptions, + user, + workDir, + } = data; + + return { + setup: { + bootstrapMethod: method, + communicationMethod: communication, + arch, + workDir, + setupScript: setup, + setupAsSudo, + }, + sshConfig: { + user, + sshKey, + authorizedKeysFile, + sshOptions, + }, + allocation: hostAllocatorSettings, + }; +}) satisfies GqlToFormFunction; + +export const formToGql = (({ allocation, setup, sshConfig }, distro) => ({ + ...distro, + arch: setup.arch, + authorizedKeysFile: sshConfig.authorizedKeysFile, + bootstrapSettings: { + // TODO: Set fields when they've been added to the form state. + clientDir: "", + communication: setup.communicationMethod, + env: [], + jasperBinaryDir: "", + jasperCredentialsPath: "", + method: setup.bootstrapMethod, + preconditionScripts: [], + resourceLimits: { + lockedMemoryKb: 0, + numFiles: 0, + numProcesses: 0, + numTasks: 0, + virtualMemoryKb: 0, + }, + rootDir: "", + serviceUser: "", + shellPath: "", + }, + hostAllocatorSettings: allocation, + setup: setup.setupScript, + setupAsSudo: setup.setupAsSudo, + sshKey: sshConfig.sshKey, + sshOptions: sshConfig.sshOptions, + user: sshConfig.user, + workDir: setup.workDir, +})) satisfies FormToGqlFunction; diff --git a/src/pages/distroSettings/tabs/HostTab/types.ts b/src/pages/distroSettings/tabs/HostTab/types.ts new file mode 100644 index 0000000000..de4bf53651 --- /dev/null +++ b/src/pages/distroSettings/tabs/HostTab/types.ts @@ -0,0 +1,42 @@ +import { + Arch, + BootstrapMethod, + CommunicationMethod, + FeedbackRule, + HostAllocatorVersion, + OverallocatedRule, + Provider, + RoundingRule, +} from "gql/generated/types"; + +export interface HostFormState { + setup: { + bootstrapMethod: BootstrapMethod; + communicationMethod: CommunicationMethod; + arch: Arch; + workDir: string; + setupScript: string; + setupAsSudo: boolean; + }; + sshConfig: { + user: string; + sshKey: string; + authorizedKeysFile: string; + sshOptions: string[]; + }; + allocation: { + version: HostAllocatorVersion; + roundingRule: RoundingRule; + feedbackRule: FeedbackRule; + hostsOverallocatedRule: OverallocatedRule; + minimumHosts: number; + maximumHosts: number; + acceptableHostIdleTime: number; + futureHostFraction: number; + }; +} + +export type TabProps = { + distroData: HostFormState; + provider: Provider; +}; diff --git a/src/pages/distroSettings/tabs/index.tsx b/src/pages/distroSettings/tabs/index.tsx index 14b74aa025..5e60b435a9 100644 --- a/src/pages/distroSettings/tabs/index.tsx +++ b/src/pages/distroSettings/tabs/index.tsx @@ -1,5 +1,6 @@ export { EventLogTab } from "./EventLogTab/EventLogTab"; export { GeneralTab } from "./GeneralTab/GeneralTab"; -export { TaskTab } from "./TaskTab/TaskTab"; +export { HostTab } from "./HostTab/HostTab"; export { ProjectTab } from "./ProjectTab/ProjectTab"; export { ProviderTab } from "./ProviderTab/ProviderTab"; +export { TaskTab } from "./TaskTab/TaskTab"; diff --git a/src/pages/distroSettings/tabs/transformers.ts b/src/pages/distroSettings/tabs/transformers.ts index cd230719ea..8025abac3e 100644 --- a/src/pages/distroSettings/tabs/transformers.ts +++ b/src/pages/distroSettings/tabs/transformers.ts @@ -1,6 +1,6 @@ import { DistroSettingsTabRoutes } from "constants/routes"; -import { DistroInput } from "gql/generated/types"; import * as general from "./GeneralTab/transformers"; +import * as host from "./HostTab/transformers"; import * as project from "./ProjectTab/transformers"; import * as provider from "./ProviderTab/transformers"; import * as task from "./TaskTab/transformers"; @@ -10,14 +10,11 @@ import { WritableDistroSettingsType, } from "./types"; -// TODO: Update maps as transformation functions are added and remove dummy return value. -const fakeReturn = {} as DistroInput; - export const formToGqlMap: { [T in WritableDistroSettingsType]: FormToGqlFunction; } = { [DistroSettingsTabRoutes.General]: general.formToGql, - [DistroSettingsTabRoutes.Host]: () => fakeReturn, + [DistroSettingsTabRoutes.Host]: host.formToGql, [DistroSettingsTabRoutes.Project]: project.formToGql, [DistroSettingsTabRoutes.Provider]: provider.formToGql, [DistroSettingsTabRoutes.Task]: task.formToGql, @@ -27,7 +24,7 @@ export const gqlToFormMap: { [T in WritableDistroSettingsType]?: GqlToFormFunction; } = { [DistroSettingsTabRoutes.General]: general.gqlToForm, - [DistroSettingsTabRoutes.Host]: () => fakeReturn, + [DistroSettingsTabRoutes.Host]: host.gqlToForm, [DistroSettingsTabRoutes.Project]: project.gqlToForm, [DistroSettingsTabRoutes.Provider]: provider.gqlToForm, [DistroSettingsTabRoutes.Task]: task.gqlToForm, diff --git a/src/pages/distroSettings/tabs/types.ts b/src/pages/distroSettings/tabs/types.ts index 345e891bbd..788f0e731f 100644 --- a/src/pages/distroSettings/tabs/types.ts +++ b/src/pages/distroSettings/tabs/types.ts @@ -1,6 +1,7 @@ import { DistroSettingsTabRoutes } from "constants/routes"; import { DistroQuery, DistroInput } from "gql/generated/types"; import { GeneralFormState } from "./GeneralTab/types"; +import { HostFormState } from "./HostTab/types"; import { ProjectFormState } from "./ProjectTab/types"; import { ProviderFormState } from "./ProviderTab/types"; import { TaskFormState } from "./TaskTab/types"; @@ -11,13 +12,12 @@ export { WritableDistroSettingsTabs }; export type WritableDistroSettingsType = (typeof WritableDistroSettingsTabs)[keyof typeof WritableDistroSettingsTabs]; -// TODO: Specify type as tabs are added. export type FormStateMap = { [T in WritableDistroSettingsType]: { [DistroSettingsTabRoutes.General]: GeneralFormState; [DistroSettingsTabRoutes.Provider]: ProviderFormState; [DistroSettingsTabRoutes.Task]: TaskFormState; - [DistroSettingsTabRoutes.Host]: any; + [DistroSettingsTabRoutes.Host]: HostFormState; [DistroSettingsTabRoutes.Project]: ProjectFormState; }[T]; }; diff --git a/src/pages/projectSettings/tabs/VariablesTab/VariableRow.tsx b/src/pages/projectSettings/tabs/VariablesTab/VariableRow.tsx index 6f1dd14557..7c1d456fbd 100644 --- a/src/pages/projectSettings/tabs/VariablesTab/VariableRow.tsx +++ b/src/pages/projectSettings/tabs/VariablesTab/VariableRow.tsx @@ -76,5 +76,6 @@ const OptionRow = styled.div` > div { flex-shrink: 0; margin-right: ${size.s}; + width: fit-content; } `;