Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
EVG-19950: Add host settings page (#2014)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophstad authored Sep 8, 2023
1 parent c084798 commit 59629b0
Show file tree
Hide file tree
Showing 19 changed files with 645 additions and 98 deletions.
60 changes: 60 additions & 0 deletions cypress/integration/distroSettings/host_section.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
10 changes: 9 additions & 1 deletion src/components/SpruceForm/ElementWrapper.tsx
Original file line number Diff line number Diff line change
@@ -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<ElementWrapperProps>`
margin-bottom: 20px;
${({ limitMaxWidth }) =>
limitMaxWidth && `max-width: ${STANDARD_FIELD_WIDTH}px;`}
`;

export default ElementWrapper;
1 change: 1 addition & 0 deletions src/components/SpruceForm/FieldTemplates/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
`;
105 changes: 47 additions & 58 deletions src/components/SpruceForm/Widgets/LeafyGreenWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -59,30 +58,26 @@ export const LeafyGreenTextInput: React.FC<
};

return (
<ElementWrapper css={elementWrapperCSS}>
<MaxWidthContainer>
<StyledTextInput
type={inputType}
data-cy={dataCy}
value={value === null || value === undefined ? "" : `${value}`}
aria-labelledby={ariaLabelledBy}
label={ariaLabelledBy ? undefined : label}
placeholder={placeholder || undefined}
description={description}
optional={optional}
disabled={disabled || readonly}
onChange={({ target }) =>
target.value === "" ? onChange(emptyValue) : onChange(target.value)
}
aria-label={label}
{...inputProps}
/>
{!!warnings?.length && (
<WarningText data-cy="input-warning">
{warnings.join(", ")}
</WarningText>
)}
</MaxWidthContainer>
<ElementWrapper limitMaxWidth css={elementWrapperCSS}>
<StyledTextInput
type={inputType}
data-cy={dataCy}
value={value === null || value === undefined ? "" : `${value}`}
aria-labelledby={ariaLabelledBy}
label={ariaLabelledBy ? undefined : label}
placeholder={placeholder || undefined}
description={description}
optional={optional}
disabled={disabled || readonly}
onChange={({ target }) =>
target.value === "" ? onChange(emptyValue) : onChange(target.value)
}
aria-label={label}
{...inputProps}
/>
{!!warnings?.length && (
<WarningText data-cy="input-warning">{warnings.join(", ")}</WarningText>
)}
</ElementWrapper>
);
};
Expand Down Expand Up @@ -183,35 +178,33 @@ export const LeafyGreenSelect: React.FC<
ariaLabelledBy ? { "aria-labelledby": ariaLabelledBy } : { label };

return (
<ElementWrapper css={elementWrapperCSS}>
<MaxWidthContainer>
<Select
allowDeselect={allowDeselect !== false}
description={description}
disabled={isDisabled}
value={value}
{...labelProps}
onChange={onChange}
placeholder={placeholder}
id={dataCy}
name={dataCy}
data-cy={dataCy}
state={hasError && !disabled ? "error" : "none"}
errorMessage={hasError ? rawErrors?.join(", ") : ""}
popoverZIndex={zIndex.dropdown}
>
{enumOptions.map((o) => {
// LG Select doesn't handle disabled options well. So we need to ensure the selected option is not disabled
const optionDisabled =
(value !== o.value && enumDisabled?.includes(o.value)) ?? false;
return (
<Option key={o.value} value={o.value} disabled={optionDisabled}>
{o.label}
</Option>
);
})}
</Select>
</MaxWidthContainer>
<ElementWrapper limitMaxWidth css={elementWrapperCSS}>
<Select
allowDeselect={allowDeselect !== false}
description={description}
disabled={isDisabled}
value={value}
{...labelProps}
onChange={onChange}
placeholder={placeholder}
id={dataCy}
name={dataCy}
data-cy={dataCy}
state={hasError && !disabled ? "error" : "none"}
errorMessage={hasError ? rawErrors?.join(", ") : ""}
popoverZIndex={zIndex.dropdown}
>
{enumOptions.map((o) => {
// LG Select doesn't handle disabled options well. So we need to ensure the selected option is not disabled
const optionDisabled =
(value !== o.value && enumDisabled?.includes(o.value)) ?? false;
return (
<Option key={o.value} value={o.value} disabled={optionDisabled}>
{o.label}
</Option>
);
})}
</Select>
</ElementWrapper>
);
};
Expand Down Expand Up @@ -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;
`;
43 changes: 20 additions & 23 deletions src/components/SpruceForm/Widgets/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<EnumSpruceWidgetProps> = ({
Expand Down Expand Up @@ -39,28 +38,26 @@ export const MultiSelect: React.FC<EnumSpruceWidgetProps> = ({
const selectedOptions = [...value, ...(includeAll ? [ALL_VALUE] : [])];

return (
<ElementWrapper css={elementWrapperCSS}>
<MaxWidthContainer>
<Container>
<Label htmlFor={`${label}-multiselect`}>{label}</Label>
<Dropdown
disabled={disabled}
id={`${label}-multiselect`}
data-cy={dataCy}
buttonText={`${label}: ${
value.length ? value.join(", ") : "No options selected."
}`}
>
<TreeSelect
onChange={handleChange}
tData={dropdownOptions}
state={selectedOptions}
hasStyling={false}
/>
</Dropdown>
{rawErrors.length > 0 && <Error>{rawErrors.join(", ")}</Error>}
</Container>
</MaxWidthContainer>
<ElementWrapper limitMaxWidth css={elementWrapperCSS}>
<Container>
<Label htmlFor={`${label}-multiselect`}>{label}</Label>
<Dropdown
disabled={disabled}
id={`${label}-multiselect`}
data-cy={dataCy}
buttonText={`${label}: ${
value.length ? value.join(", ") : "No options selected."
}`}
>
<TreeSelect
onChange={handleChange}
tData={dropdownOptions}
state={selectedOptions}
hasStyling={false}
/>
</Dropdown>
{rawErrors.length > 0 && <Error>{rawErrors.join(", ")}</Error>}
</Container>
</ElementWrapper>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/gql/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down
6 changes: 6 additions & 0 deletions src/gql/mocks/getSpruceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions src/gql/queries/get-spruce-config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ query SpruceConfig {
jira {
host
}
keys {
location
name
}
providers {
aws {
maxVolumeSizePerUser
Expand Down
4 changes: 2 additions & 2 deletions src/pages/distroSettings/HeaderButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -64,7 +64,7 @@ export const HeaderButtons: React.FC<Props> = ({ 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<typeof tab> = formToGqlMap[tab];
const changes = formToGql(formData, distro);
saveDistro({
variables: {
Expand Down
18 changes: 13 additions & 5 deletions src/pages/distroSettings/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand All @@ -26,7 +28,6 @@ export const DistroSettingsTabs: React.FC<Props> = ({ distro }) => {
const tabData = useMemo(() => getTabData(distro), [distro]);

useEffect(() => {
// @ts-expect-error TODO: Type when all tabs have been implemented
setInitialData(tabData);
}, [setInitialData, tabData]);

Expand Down Expand Up @@ -59,6 +60,15 @@ export const DistroSettingsTabs: React.FC<Props> = ({ distro }) => {
<TaskTab distroData={tabData[DistroSettingsTabRoutes.Task]} />
}
/>
<Route
path={DistroSettingsTabRoutes.Host}
element={
<HostTab
distroData={tabData[DistroSettingsTabRoutes.Host]}
provider={distro.provider}
/>
}
/>
<Route
path={DistroSettingsTabRoutes.Project}
element={
Expand All @@ -74,15 +84,13 @@ export const DistroSettingsTabs: React.FC<Props> = ({ 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`
Expand Down
17 changes: 17 additions & 0 deletions src/pages/distroSettings/tabs/HostTab/HostTab.tsx
Original file line number Diff line number Diff line change
@@ -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<TabProps> = ({ distroData, provider }) => {
const spruceConfig = useSpruceConfig();
const sshKeys = spruceConfig?.keys;

const formSchema = useMemo(
() => getFormSchema({ provider, sshKeys }),
[provider, sshKeys]
);

return <BaseTab formSchema={formSchema} initialFormState={distroData} />;
};
Loading

0 comments on commit 59629b0

Please sign in to comment.