From b65f8c81ea6fd84a05a2c5392ecf87d1fea14f8c Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Tue, 26 Nov 2024 09:42:51 -0500
Subject: [PATCH 1/7] Dropdown work and cleanups only
---
.../TeamsDropdown/TeamsDropdown.tsx | 2 +-
.../DropdownOptionTooltipWrapper/_styles.scss | 5 +-
.../DropdownWrapper.stories.tsx | 105 ++++++
.../DropdownWrapper/DropdownWrapper.tsx | 325 ++++++++++++++++++
.../forms/fields/DropdownWrapper/_styles.scss | 0
.../forms/fields/DropdownWrapper/index.tsx | 1 +
.../validators/validate_presence/index.ts | 2 +-
frontend/interfaces/role.ts | 16 -
.../UsersPage/UsersPage.tsx | 17 +-
.../UserManagementPage/UserManagementPage.tsx | 14 +-
.../SelectRoleForm/SelectRoleForm.tsx | 64 ++--
.../SelectedTeamsForm/SelectedTeamsForm.tsx | 22 +-
.../components/SelectedTeamsForm/_styles.scss | 2 +-
.../helpers/userManagementHelpers.ts | 13 +-
14 files changed, 488 insertions(+), 100 deletions(-)
create mode 100644 frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
create mode 100644 frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
create mode 100644 frontend/components/forms/fields/DropdownWrapper/_styles.scss
create mode 100644 frontend/components/forms/fields/DropdownWrapper/index.tsx
delete mode 100644 frontend/interfaces/role.ts
diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
index e38b1c1b13f3..bc59c6fb0c67 100644
--- a/frontend/components/TeamsDropdown/TeamsDropdown.tsx
+++ b/frontend/components/TeamsDropdown/TeamsDropdown.tsx
@@ -18,7 +18,7 @@ import {
import Icon from "components/Icon";
-interface INumberDropdownOption extends Omit {
+export interface INumberDropdownOption extends Omit {
value: number; // Redefine the value property to be just number
}
diff --git a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
index 7fcd24ad33ed..23bf82f07066 100644
--- a/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
+++ b/frontend/components/forms/fields/Dropdown/DropdownOptionTooltipWrapper/_styles.scss
@@ -1,7 +1,8 @@
// Used with old react-select dropdown and
-// New react-select-5 ActionsDropdown.tsx
+// New react-select-5: ActionsDropdown.tsx, DropdownWrapper.tsx
.Select > .Select-menu-outer,
-.actions-dropdown {
+.actions-dropdown,
+.react-select__option {
.is-disabled * {
color: $ui-fleet-black-50;
}
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
new file mode 100644
index 000000000000..0a2b390799f0
--- /dev/null
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
@@ -0,0 +1,105 @@
+import React from "react";
+import { Meta, Story } from "@storybook/react";
+import DropdownWrapper, { IDropdownWrapper } from "./DropdownWrapper";
+
+export default {
+ title: "Components/Forms/DropdownWrapper",
+ component: DropdownWrapper,
+ argTypes: {
+ onChange: { action: "changed" },
+ },
+} as Meta;
+
+const Template: Story = (args) => (
+
+);
+
+export const Default = Template.bind({});
+Default.args = {
+ options: [
+ { label: "Option 1", value: "observer", isDisabled: false },
+ { label: "Option 2", value: "maintainer", isDisabled: false },
+ { label: "Option 3", value: "admin", isDisabled: false },
+ ],
+ value: null,
+ name: "default-dropdown",
+ label: "Default Dropdown",
+};
+
+export const WithSearchable = Template.bind({});
+WithSearchable.args = {
+ ...Default.args,
+ isSearchable: true,
+ name: "searchable-dropdown",
+ label: "Searchable Dropdown",
+};
+
+export const Disabled = Template.bind({});
+Disabled.args = {
+ ...Default.args,
+ isDisabled: true,
+ name: "disabled-dropdown",
+ label: "Disabled Dropdown",
+};
+
+export const WithError = Template.bind({});
+WithError.args = {
+ ...Default.args,
+ error: "This is an error message",
+ name: "error-dropdown",
+ label: "Dropdown with Error",
+};
+
+export const WithTooltip = Template.bind({});
+WithTooltip.args = {
+ options: [
+ {
+ label: "Option 1",
+ value: "Observer",
+ helpText: "This is help text for Option 1",
+ isDisabled: false,
+ },
+ {
+ label: "Option 2",
+ value: "Maintainer",
+ helpText: "This is help text for Option 2",
+ isDisabled: false,
+ },
+ {
+ label: "Option 3",
+ value: "Admin",
+ helpText: "This is help text for Option 3",
+ isDisabled: false,
+ },
+ ],
+ value: null,
+ name: "tooltip-dropdown",
+ label: "Dropdown with Tooltips",
+};
+
+export const WithHelpText = Template.bind({});
+WithHelpText.args = {
+ options: [
+ {
+ label: "Option 1",
+ value: "observer",
+ helpText: "This is help text for Option 1",
+ isDisabled: false,
+ },
+ {
+ label: "Option 2",
+ value: "maintainer",
+ helpText: "This is help text for Option 2",
+ isDisabled: false,
+ },
+ {
+ label: "Option 3",
+ value: "admin",
+ helpText: "This is help text for Option 3",
+ isDisabled: false,
+ },
+ ],
+ value: null,
+ name: "helptext-dropdown",
+ label: "Dropdown with Help Text",
+};
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
new file mode 100644
index 000000000000..2fa9261c7c85
--- /dev/null
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -0,0 +1,325 @@
+/**
+ * This is a new component built off react-select 5.4
+ * meant to replace Dropdown.jsx built off react-select 1.3
+ *
+ * Prototyped on UserForm.tsx but added and tested the following:
+ * Options: text, disabled, helptext,
+ * Label: text
+ */
+
+import classnames from "classnames";
+import React from "react";
+import Select, {
+ StylesConfig,
+ DropdownIndicatorProps,
+ OptionProps,
+ components,
+ PropsValue,
+ SingleValue,
+} from "react-select-5";
+
+import { COLORS } from "styles/var/colors";
+import { PADDING } from "styles/var/padding";
+
+import FormField from "components/forms/FormField";
+import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
+import Icon from "components/Icon";
+
+const getOptionBackgroundColor = (state: any) => {
+ return state.isSelected || state.isFocused
+ ? COLORS["ui-vibrant-blue-10"]
+ : "transparent";
+};
+
+export interface CustomOptionType {
+ label: string;
+ value: string;
+ tooltipContent?: string;
+ helpText?: string;
+ isDisabled?: boolean;
+}
+
+export interface IDropdownWrapper {
+ options: CustomOptionType[];
+ value?: PropsValue | string;
+ onChange: (newValue: SingleValue) => void;
+ name: string;
+ className?: string;
+ labelClassname?: string;
+ error?: string;
+ label?: JSX.Element | string;
+ helpText?: JSX.Element | string;
+ isSearchable?: boolean;
+ isDisabled?: boolean;
+}
+
+const baseClass = "dropdown-wrapper";
+
+const DropdownWrapper = ({
+ options,
+ value,
+ onChange,
+ name,
+ className,
+ labelClassname,
+ error,
+ label,
+ helpText,
+ isSearchable,
+ isDisabled = false,
+}: IDropdownWrapper) => {
+ console.log("onChange", onChange);
+ console.log("value", value);
+ console.log("options", options);
+ const wrapperClassNames = classnames(baseClass, className);
+
+ const handleChange = (newValue: SingleValue) => {
+ onChange(newValue);
+ };
+
+ // Ability to handle value of type string or CustomOptionType
+ const getCurrentValue = () => {
+ if (typeof value === "string") {
+ return options.find((option) => option.value === value) || null;
+ }
+ return value;
+ };
+
+ interface CustomOptionProps
+ extends Omit, "data"> {
+ data: CustomOptionType;
+ }
+
+ const CustomOption = (props: CustomOptionProps) => {
+ const { data, ...rest } = props;
+
+ const optionContent = (
+
+ {data.label}
+ {data.helpText && (
+ {data.helpText}
+ )}
+
+ );
+
+ return (
+
+ {data.tooltipContent ? (
+
+ {optionContent}
+
+ ) : (
+ optionContent
+ )}
+
+ );
+ };
+
+ const CustomDropdownIndicator = (
+ props: DropdownIndicatorProps
+ ) => {
+ const { isFocused, selectProps } = props;
+ const color =
+ isFocused || selectProps.menuIsOpen
+ ? "core-fleet-blue"
+ : "core-fleet-black";
+
+ return (
+
+
+
+ );
+ };
+
+ const customStyles: StylesConfig = {
+ container: (provided) => ({
+ ...provided,
+ width: "100%",
+ height: "40px",
+ }),
+ control: (provided, state) => ({
+ ...provided,
+ display: "flex",
+ flexDirection: "row",
+ width: "100%",
+ backgroundColor: COLORS["ui-off-white"],
+ paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px)
+ paddingRight: "8px",
+ cursor: "pointer",
+ boxShadow: "none",
+ borderRadius: "4px",
+ borderColor: state.isFocused
+ ? COLORS["core-fleet-blue"]
+ : COLORS["ui-fleet-black-10"],
+ "&:hover": {
+ boxShadow: "none",
+ borderColor: COLORS["core-fleet-blue"],
+ ".dropdown-wrapper__single-value": {
+ color: COLORS["core-vibrant-blue-over"],
+ },
+ ".dropdown-wrapper__indicator path": {
+ stroke: COLORS["core-vibrant-blue-over"],
+ },
+ },
+ // When tabbing
+ // Relies on --is-focused for styling as &:focus-visible cannot be applied
+ "&.dropdown-wrapper__control--is-focused": {
+ ".dropdown-wrapper__single-value": {
+ color: COLORS["core-vibrant-blue-over"],
+ },
+ ".dropdown-wrapper__indicator path": {
+ stroke: COLORS["core-vibrant-blue-over"],
+ },
+ },
+ ...(state.isDisabled && {
+ ".dropdown-wrapper__single-value": {
+ color: COLORS["ui-fleet-black-50"],
+ },
+ ".dropdown-wrapper__indicator path": {
+ stroke: COLORS["ui-fleet-black-50"],
+ },
+ }),
+ "&:active": {
+ ".dropdown-wrapper__single-value": {
+ color: COLORS["core-vibrant-blue-down"],
+ },
+ ".dropdown-wrapper__indicator path": {
+ stroke: COLORS["core-vibrant-blue-down"],
+ },
+ },
+ ...(state.menuIsOpen && {
+ ".dropdown-wrapper__indicator svg": {
+ transform: "rotate(180deg)",
+ transition: "transform 0.25s ease",
+ },
+ }),
+ }),
+ singleValue: (provided) => ({
+ ...provided,
+ fontSize: "16px",
+ margin: 0,
+ padding: 0,
+ }),
+ dropdownIndicator: (provided) => ({
+ ...provided,
+ display: "flex",
+ padding: "2px",
+ svg: {
+ transition: "transform 0.25s ease",
+ },
+ }),
+ menu: (provided) => ({
+ ...provided,
+ boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
+ borderRadius: "4px",
+ zIndex: 6,
+ overflow: "hidden",
+ border: 0,
+ marginTop: 0,
+ maxHeight: "none",
+ position: "absolute",
+ left: "0",
+ animation: "fade-in 150ms ease-out",
+ }),
+ menuList: (provided) => ({
+ ...provided,
+ padding: PADDING["pad-small"],
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: 0,
+ }),
+ option: (provided, state) => ({
+ ...provided,
+ padding: "10px 8px",
+ fontSize: "14px",
+ backgroundColor: getOptionBackgroundColor(state),
+ color: COLORS["core-fleet-black"],
+ "&:hover": {
+ backgroundColor: state.isDisabled
+ ? "transparent"
+ : COLORS["ui-vibrant-blue-10"],
+ },
+ "&:active": {
+ backgroundColor: state.isDisabled
+ ? "transparent"
+ : COLORS["ui-vibrant-blue-10"],
+ },
+ ...(state.isDisabled && {
+ color: COLORS["ui-fleet-black-50"],
+ fontStyle: "italic",
+ cursor: "not-allowed",
+ pointerEvents: "none",
+ }),
+ // Styles for custom option
+ ".dropdown-wrapper__option": {
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ width: "100%",
+ },
+ ".dropdown-wrapper__help-text": {
+ fontSize: "12px",
+ whiteSpace: "normal",
+ color: COLORS["ui-fleet-black-50"],
+ fontStyle: "italic",
+ },
+ }),
+ menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections
+ };
+
+ const renderLabel = () => {
+ const labelWrapperClasses = classnames(
+ `${baseClass}__label`,
+ labelClassname,
+ { [`${baseClass}__label--error`]: !!error }
+ );
+
+ if (!label) {
+ return "";
+ }
+
+ return (
+
+ );
+ };
+
+ return (
+
+
+ );
+};
+
+export default DropdownWrapper;
diff --git a/frontend/components/forms/fields/DropdownWrapper/_styles.scss b/frontend/components/forms/fields/DropdownWrapper/_styles.scss
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/frontend/components/forms/fields/DropdownWrapper/index.tsx b/frontend/components/forms/fields/DropdownWrapper/index.tsx
new file mode 100644
index 000000000000..7e83f801949e
--- /dev/null
+++ b/frontend/components/forms/fields/DropdownWrapper/index.tsx
@@ -0,0 +1 @@
+export { default } from "./DropdownWrapper";
diff --git a/frontend/components/forms/validators/validate_presence/index.ts b/frontend/components/forms/validators/validate_presence/index.ts
index 9f422bef198d..2ac019f4a8c4 100644
--- a/frontend/components/forms/validators/validate_presence/index.ts
+++ b/frontend/components/forms/validators/validate_presence/index.ts
@@ -1,3 +1,3 @@
export default (actual: any): boolean => {
- return !!actual;
+ return actual !== null && actual !== undefined && actual.trim() !== "";
};
diff --git a/frontend/interfaces/role.ts b/frontend/interfaces/role.ts
deleted file mode 100644
index b645baa50db7..000000000000
--- a/frontend/interfaces/role.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import PropTypes from "prop-types";
-import { UserRole } from "./user";
-
-export default PropTypes.shape({
- disabled: PropTypes.bool,
- label: PropTypes.string,
- value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
- helpText: PropTypes.string,
-});
-
-export interface IRole {
- disabled: boolean;
- label: string;
- value: UserRole;
- helpText?: string;
-}
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
index 63bb85c823b8..96bb6b44f55c 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
@@ -1,10 +1,4 @@
-import React, {
- useCallback,
- useContext,
- useMemo,
- useState,
- useEffect,
-} from "react";
+import React, { useCallback, useContext, useMemo, useState } from "react";
import { useQuery } from "react-query";
import { Link } from "react-router";
@@ -25,7 +19,7 @@ import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
import Spinner from "components/Spinner";
import TableCount from "components/TableContainer/TableCount";
-import CreateUserModal from "pages/admin/UserManagementPage/components/CreateUserModal";
+import AddUserModal from "pages/admin/UserManagementPage/components/AddUserModal";
import EditUserModal from "../../../UserManagementPage/components/EditUserModal";
import {
IFormData,
@@ -469,6 +463,9 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
sesConfigured={sesConfigured}
canUseSso={canUseSso}
isSsoEnabled={userEditing?.sso_enabled}
+ isTwoFactorAuthenticationEnabled={
+ userEditing?.two_factor_authentication_enabled
+ }
isModifiedByGlobalAdmin={isGlobalAdmin}
currentTeam={currentTeamDetails}
isUpdatingUsers={isUpdatingUsers}
@@ -476,8 +473,8 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
/>
)}
{showCreateUserModal && currentTeamDetails && (
- {
Create new users, customize user permissions, and remove users from
Fleet.
- (
-
- )}
- >
-
-
+
);
};
diff --git a/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx b/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
index a81d05df4421..4ddd800a9939 100644
--- a/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
@@ -1,10 +1,9 @@
import React, { useState, useContext } from "react";
-
import { ITeam } from "interfaces/team";
import { UserRole } from "interfaces/user";
-// ignore TS error for now until these are rewritten in ts.
-// @ts-ignore
-import Dropdown from "components/forms/fields/Dropdown";
+import { SingleValue } from "react-select-5";
+import DropdownWrapper from "components/forms/fields/DropdownWrapper";
+import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import { AppContext } from "context/app";
import { roleOptions } from "../../helpers/userManagementHelpers";
@@ -19,23 +18,14 @@ interface ISelectRoleFormProps {
const generateSelectedTeamData = (
allTeams: ITeam[],
- updatedTeam?: any
+ updatedTeam?: Partial
): ITeam[] => {
- const filtered = allTeams.map(
- (teamItem): ITeam => {
- const teamRole =
- teamItem.id === updatedTeam?.id ? updatedTeam.role : teamItem.role;
- return {
- description: teamItem.description,
- id: teamItem.id,
- host_count: teamItem.host_count,
- user_count: teamItem.user_count,
- name: teamItem.name,
- role: teamRole,
- };
- }
+ return allTeams.map(
+ (teamItem): ITeam => ({
+ ...teamItem,
+ role: teamItem.id === updatedTeam?.id ? updatedTeam.role! : teamItem.role,
+ })
);
- return filtered;
};
const SelectRoleForm = ({
@@ -43,33 +33,33 @@ const SelectRoleForm = ({
currentTeam,
teams,
onFormChange,
- label,
isApiOnly,
}: ISelectRoleFormProps): JSX.Element => {
const { isPremiumTier } = useContext(AppContext);
- const [selectedRole, setSelectedRole] = useState(
- defaultTeamRole.toLowerCase()
- );
-
- const updateSelectedRole = (newRoleValue: UserRole) => {
- const updatedTeam = { ...currentTeam };
-
- updatedTeam.role = newRoleValue;
+ const [selectedRole, setSelectedRole] = useState({
+ value: defaultTeamRole.toLowerCase(),
+ label: defaultTeamRole,
+ });
- onFormChange(generateSelectedTeamData(teams, updatedTeam));
-
- setSelectedRole(newRoleValue);
+ const updateSelectedRole = (newRoleValue: SingleValue) => {
+ if (newRoleValue) {
+ const updatedTeam = {
+ ...currentTeam,
+ role: newRoleValue.value as UserRole,
+ };
+ onFormChange(generateSelectedTeamData(teams, updatedTeam));
+ setSelectedRole(newRoleValue);
+ }
};
return (
- updateSelectedRole(newRoleValue)}
- testId={`${name}-checkbox`}
+ value={selectedRole}
+ onChange={updateSelectedRole}
+ isSearchable={false}
/>
);
};
diff --git a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
index 86ffa85ee5ad..5a735d5ede26 100644
--- a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
@@ -3,8 +3,9 @@ import React, { useState } from "react";
import { ITeam } from "interfaces/team";
import { UserRole } from "interfaces/user";
import Checkbox from "components/forms/fields/Checkbox";
-// @ts-ignore
-import Dropdown from "components/forms/fields/Dropdown";
+import { SingleValue } from "react-select-5";
+import DropdownWrapper from "components/forms/fields/DropdownWrapper";
+import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import { roleOptions } from "../../helpers/userManagementHelpers";
interface ITeamCheckboxListItem extends ITeam {
@@ -62,7 +63,7 @@ const generateSelectedTeamData = (
const updateFormState = (
prevTeamItems: ITeamCheckboxListItem[],
teamId: number,
- newValue: UserRole | boolean | undefined
+ newValue: SingleValue | boolean | undefined
): ITeamCheckboxListItem[] => {
const prevItemIndex = prevTeamItems.findIndex((item) => item.id === teamId);
const prevItem = prevTeamItems[prevItemIndex];
@@ -70,7 +71,7 @@ const updateFormState = (
if (typeof newValue === "boolean") {
prevItem.isChecked = newValue;
} else {
- prevItem.role = newValue;
+ prevItem.role = newValue?.value as UserRole;
}
return [...prevTeamItems];
@@ -87,7 +88,7 @@ const useSelectedTeamState = (
const updateSelectedTeams = (
teamId: number,
- newValue: UserRole | boolean
+ newValue: CustomOptionType | boolean
) => {
setTeamsFormList((prevState) => {
const updatedTeamFormList = updateFormState(prevState, teamId, newValue);
@@ -127,15 +128,16 @@ const SelectedTeamsForm = ({
>
{name}
-
- updateSelectedTeams(teamItem.id, newValue)
+ isSearchable={false}
+ onChange={(newValue: SingleValue) =>
+ updateSelectedTeams(teamItem.id, newValue as CustomOptionType)
}
- testId={`${name}-checkbox`}
+ // testId={`${name}-checkbox`}
/>
);
diff --git a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss
index 2fc392ae2124..dc2629b80fd2 100644
--- a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss
+++ b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss
@@ -13,7 +13,7 @@
justify-content: space-between;
.form-field--dropdown {
- width: auto;
+ width: 154px; // Matches dropdown
}
}
diff --git a/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.ts b/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.ts
index 60bc3ba6c3ad..60ba9120b8a8 100644
--- a/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.ts
+++ b/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.ts
@@ -1,8 +1,8 @@
import { isEqual } from "lodash";
+import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import { IInvite } from "interfaces/invite";
import { IUser, IUserUpdateBody, IUpdateUserFormData } from "interfaces/user";
-import { IRole } from "interfaces/role";
import { IFormData } from "../components/UserForm/UserForm";
type ICurrentUserData = Pick<
@@ -10,7 +10,7 @@ type ICurrentUserData = Pick<
"global_role" | "teams" | "name" | "email" | "sso_enabled"
>;
-interface IRoleOptionsParams {
+export interface IRoleOptionsParams {
isPremiumTier?: boolean;
isApiOnly?: boolean;
}
@@ -58,20 +58,17 @@ const generateUpdateData = (
export const roleOptions = ({
isPremiumTier,
isApiOnly,
-}: IRoleOptionsParams): IRole[] => {
- const roles: IRole[] = [
+}: IRoleOptionsParams): CustomOptionType[] => {
+ const roles: CustomOptionType[] = [
{
- disabled: false,
label: "Observer",
value: "observer",
},
{
- disabled: false,
label: "Maintainer",
value: "maintainer",
},
{
- disabled: false,
label: "Admin",
value: "admin",
},
@@ -79,14 +76,12 @@ export const roleOptions = ({
if (isPremiumTier) {
roles.splice(1, 0, {
- disabled: false,
label: "Observer+",
value: "observer_plus",
});
if (isApiOnly) {
roles.splice(3, 0, {
- disabled: false,
label: "GitOps",
value: "gitops",
});
From b156b0706610cc46f250cf7ac5ab204a984bcd01 Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Tue, 26 Nov 2024 09:50:11 -0500
Subject: [PATCH 2/7] Fix imports
---
.../TeamDetailsWrapper/UsersPage/UsersPage.tsx | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
index 96bb6b44f55c..a60b3f0661ed 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/UsersPage/UsersPage.tsx
@@ -19,7 +19,7 @@ import TableContainer from "components/TableContainer";
import TableDataError from "components/DataError";
import Spinner from "components/Spinner";
import TableCount from "components/TableContainer/TableCount";
-import AddUserModal from "pages/admin/UserManagementPage/components/AddUserModal";
+import CreateUserModal from "pages/admin/UserManagementPage/components/CreateUserModal";
import EditUserModal from "../../../UserManagementPage/components/EditUserModal";
import {
IFormData,
@@ -463,9 +463,6 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
sesConfigured={sesConfigured}
canUseSso={canUseSso}
isSsoEnabled={userEditing?.sso_enabled}
- isTwoFactorAuthenticationEnabled={
- userEditing?.two_factor_authentication_enabled
- }
isModifiedByGlobalAdmin={isGlobalAdmin}
currentTeam={currentTeamDetails}
isUpdatingUsers={isUpdatingUsers}
@@ -473,8 +470,8 @@ const UsersPage = ({ location, router }: ITeamSubnavProps): JSX.Element => {
/>
)}
{showCreateUserModal && currentTeamDetails && (
-
Date: Tue, 26 Nov 2024 09:56:16 -0500
Subject: [PATCH 3/7] More cleanup
---
.../components/forms/fields/DropdownWrapper/DropdownWrapper.tsx | 2 +-
.../components/SelectedTeamsForm/SelectedTeamsForm.tsx | 1 -
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
index 2fa9261c7c85..75e7187a5a2a 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -3,7 +3,7 @@
* meant to replace Dropdown.jsx built off react-select 1.3
*
* Prototyped on UserForm.tsx but added and tested the following:
- * Options: text, disabled, helptext,
+ * Options: text, disabled, helptext, tooltip
* Label: text
*/
diff --git a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
index 5a735d5ede26..4f8635f995a2 100644
--- a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/SelectedTeamsForm.tsx
@@ -137,7 +137,6 @@ const SelectedTeamsForm = ({
onChange={(newValue: SingleValue) =>
updateSelectedTeams(teamItem.id, newValue as CustomOptionType)
}
- // testId={`${name}-checkbox`}
/>
);
From ee88c36be439ca1d9b7819b28429d76b1f6d750d Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Tue, 26 Nov 2024 10:37:14 -0500
Subject: [PATCH 4/7] Fix disabled and error label color
---
.../DropdownWrapper.stories.tsx | 109 ++++++------------
.../DropdownWrapper/DropdownWrapper.tsx | 14 ++-
.../forms/fields/DropdownWrapper/_styles.scss | 12 ++
.../validators/validate_presence/index.ts | 2 +-
4 files changed, 54 insertions(+), 83 deletions(-)
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
index 0a2b390799f0..ee0d223c58ca 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx
@@ -1,105 +1,62 @@
+// stories/DropdownWrapper.stories.tsx
+
import React from "react";
import { Meta, Story } from "@storybook/react";
-import DropdownWrapper, { IDropdownWrapper } from "./DropdownWrapper";
+import DropdownWrapper, {
+ IDropdownWrapper,
+ CustomOptionType,
+} from "./DropdownWrapper";
+// Define metadata for the story
export default {
- title: "Components/Forms/DropdownWrapper",
+ title: "Components/DropdownWrapper",
component: DropdownWrapper,
argTypes: {
onChange: { action: "changed" },
},
} as Meta;
+// Define a template for the stories
const Template: Story = (args) => (
);
+// Sample options to be used in the dropdown
+const sampleOptions: CustomOptionType[] = [
+ { label: "Option 1", value: "option1", helpText: "Help text for option 1" },
+ {
+ label: "Option 2",
+ value: "option2",
+ tooltipContent: "Tooltip for option 2",
+ },
+ { label: "Option 3", value: "option3", isDisabled: true },
+];
+
+// Default story
export const Default = Template.bind({});
Default.args = {
- options: [
- { label: "Option 1", value: "observer", isDisabled: false },
- { label: "Option 2", value: "maintainer", isDisabled: false },
- { label: "Option 3", value: "admin", isDisabled: false },
- ],
- value: null,
- name: "default-dropdown",
- label: "Default Dropdown",
-};
-
-export const WithSearchable = Template.bind({});
-WithSearchable.args = {
- ...Default.args,
- isSearchable: true,
- name: "searchable-dropdown",
- label: "Searchable Dropdown",
+ options: sampleOptions,
+ name: "dropdown-example",
+ label: "Select an option",
};
+// Disabled story
export const Disabled = Template.bind({});
Disabled.args = {
...Default.args,
isDisabled: true,
- name: "disabled-dropdown",
- label: "Disabled Dropdown",
};
+// With Help Text story
+export const WithHelpText = Template.bind({});
+WithHelpText.args = {
+ ...Default.args,
+ helpText: "This is some help text for the dropdown",
+};
+
+// With Error story
export const WithError = Template.bind({});
WithError.args = {
...Default.args,
error: "This is an error message",
- name: "error-dropdown",
- label: "Dropdown with Error",
-};
-
-export const WithTooltip = Template.bind({});
-WithTooltip.args = {
- options: [
- {
- label: "Option 1",
- value: "Observer",
- helpText: "This is help text for Option 1",
- isDisabled: false,
- },
- {
- label: "Option 2",
- value: "Maintainer",
- helpText: "This is help text for Option 2",
- isDisabled: false,
- },
- {
- label: "Option 3",
- value: "Admin",
- helpText: "This is help text for Option 3",
- isDisabled: false,
- },
- ],
- value: null,
- name: "tooltip-dropdown",
- label: "Dropdown with Tooltips",
-};
-
-export const WithHelpText = Template.bind({});
-WithHelpText.args = {
- options: [
- {
- label: "Option 1",
- value: "observer",
- helpText: "This is help text for Option 1",
- isDisabled: false,
- },
- {
- label: "Option 2",
- value: "maintainer",
- helpText: "This is help text for Option 2",
- isDisabled: false,
- },
- {
- label: "Option 3",
- value: "admin",
- helpText: "This is help text for Option 3",
- isDisabled: false,
- },
- ],
- value: null,
- name: "helptext-dropdown",
- label: "Dropdown with Help Text",
};
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
index 75e7187a5a2a..f14ead0f9c59 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -2,9 +2,11 @@
* This is a new component built off react-select 5.4
* meant to replace Dropdown.jsx built off react-select 1.3
*
+ * See storybook component for current functionality
+ *
* Prototyped on UserForm.tsx but added and tested the following:
- * Options: text, disabled, helptext, tooltip
- * Label: text
+ * Options: text, disabled, option helptext, option tooltip
+ * Other: label text, dropdown help text, dropdown error
*/
import classnames from "classnames";
@@ -68,9 +70,6 @@ const DropdownWrapper = ({
isSearchable,
isDisabled = false,
}: IDropdownWrapper) => {
- console.log("onChange", onChange);
- console.log("value", value);
- console.log("options", options);
const wrapperClassNames = classnames(baseClass, className);
const handleChange = (newValue: SingleValue) => {
@@ -279,7 +278,10 @@ const DropdownWrapper = ({
const labelWrapperClasses = classnames(
`${baseClass}__label`,
labelClassname,
- { [`${baseClass}__label--error`]: !!error }
+ {
+ [`${baseClass}__label--error`]: !!error,
+ [`${baseClass}__label--disabled`]: isDisabled,
+ }
);
if (!label) {
diff --git a/frontend/components/forms/fields/DropdownWrapper/_styles.scss b/frontend/components/forms/fields/DropdownWrapper/_styles.scss
index e69de29bb2d1..8bd7de893720 100644
--- a/frontend/components/forms/fields/DropdownWrapper/_styles.scss
+++ b/frontend/components/forms/fields/DropdownWrapper/_styles.scss
@@ -0,0 +1,12 @@
+// react-select's customStyles styles the internal components
+// and not external elements like labels
+.dropdown-wrapper {
+ &__label {
+ &--error {
+ color: $ui-error;
+ }
+ &--disabled {
+ color: $ui-fleet-black-50;
+ }
+ }
+}
diff --git a/frontend/components/forms/validators/validate_presence/index.ts b/frontend/components/forms/validators/validate_presence/index.ts
index 2ac019f4a8c4..9f422bef198d 100644
--- a/frontend/components/forms/validators/validate_presence/index.ts
+++ b/frontend/components/forms/validators/validate_presence/index.ts
@@ -1,3 +1,3 @@
export default (actual: any): boolean => {
- return actual !== null && actual !== undefined && actual.trim() !== "";
+ return !!actual;
};
From 14ae80c864f2639c9b288890cb3cdb38656bd1df Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Tue, 26 Nov 2024 10:47:45 -0500
Subject: [PATCH 5/7] Update empty searchable to match current empty searchable
string and styling
---
.../forms/fields/DropdownWrapper/DropdownWrapper.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
index f14ead0f9c59..50cff488fc7f 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -272,6 +272,12 @@ const DropdownWrapper = ({
},
}),
menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections
+ noOptionsMessage: (provided) => ({
+ ...provided,
+ textAlign: "left",
+ fontSize: "14px",
+ padding: "10px 8px",
+ }),
};
const renderLabel = () => {
@@ -318,6 +324,7 @@ const DropdownWrapper = ({
onChange={handleChange}
isDisabled={isDisabled}
menuPortalTarget={document.body}
+ noOptionsMessage={() => "No results found"}
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
/>
From 67dc67532b4ea011e182b2dd5218e285b5eb4aed Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Mon, 2 Dec 2024 15:59:17 -0500
Subject: [PATCH 6/7] Add placeholder and tests
---
.../DropdownWrapper/DropdownWrapper.tests.tsx | 102 ++++++++++++++++++
.../DropdownWrapper/DropdownWrapper.tsx | 3 +
2 files changed, 105 insertions(+)
create mode 100644 frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx
new file mode 100644
index 000000000000..2715876f8ba4
--- /dev/null
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tests.tsx
@@ -0,0 +1,102 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import DropdownWrapper, { CustomOptionType } from "./DropdownWrapper";
+
+const sampleOptions: CustomOptionType[] = [
+ {
+ label: "Option 1",
+ value: "option1",
+ tooltipContent: "Tooltip 1",
+ helpText: "Help text 1",
+ },
+ {
+ label: "Option 2",
+ value: "option2",
+ tooltipContent: "Tooltip 2",
+ helpText: "Help text 2",
+ },
+];
+
+describe("DropdownWrapper Component", () => {
+ const mockOnChange = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("renders with help text", () => {
+ render(
+
+ );
+
+ expect(screen.getByText(/test dropdown/i)).toBeInTheDocument();
+ expect(screen.getByText(/this is a help text/i)).toBeInTheDocument();
+ });
+
+ test("calls onChange when an option is selected", async () => {
+ render(
+
+ );
+
+ // Open the dropdown
+ await userEvent.click(screen.getByText(/option 1/i));
+
+ // Select Option 2
+ await userEvent.click(screen.getByText(/option 2/i));
+
+ expect(mockOnChange).toHaveBeenCalledWith({
+ helpText: "Help text 2",
+ label: "Option 2",
+ tooltipContent: "Tooltip 2",
+ value: "option2",
+ });
+ });
+
+ test("renders error message when provided", () => {
+ render(
+
+ );
+
+ expect(screen.getByText(/this is an error message/i)).toBeInTheDocument();
+ });
+
+ test("displays no options message when no options are available", async () => {
+ render(
+
+ );
+
+ // Open dropdown
+ await userEvent.click(screen.getByText(/choose option/i));
+
+ expect(screen.getByText(/no results found/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
index 50cff488fc7f..c74ec754585c 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -53,6 +53,7 @@ export interface IDropdownWrapper {
helpText?: JSX.Element | string;
isSearchable?: boolean;
isDisabled?: boolean;
+ placeholder?: string;
}
const baseClass = "dropdown-wrapper";
@@ -69,6 +70,7 @@ const DropdownWrapper = ({
helpText,
isSearchable,
isDisabled = false,
+ placeholder,
}: IDropdownWrapper) => {
const wrapperClassNames = classnames(baseClass, className);
@@ -326,6 +328,7 @@ const DropdownWrapper = ({
menuPortalTarget={document.body}
noOptionsMessage={() => "No results found"}
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
+ placeholder={placeholder}
/>
);
From afdff184303a80241980d69c20c03267e97b8849 Mon Sep 17 00:00:00 2001
From: RachelElysia
Date: Mon, 2 Dec 2024 16:37:27 -0500
Subject: [PATCH 7/7] Updates re: review comments
---
.../forms/fields/DropdownWrapper/DropdownWrapper.tsx | 7 +++++--
.../components/forms/fields/DropdownWrapper/_styles.scss | 6 ++++--
.../components/SelectRoleForm/SelectRoleForm.tsx | 1 -
.../UserManagementPage/components/UserForm/UserForm.tsx | 1 -
4 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
index c74ec754585c..51c5c3cd695f 100644
--- a/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
+++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx
@@ -54,6 +54,7 @@ export interface IDropdownWrapper {
isSearchable?: boolean;
isDisabled?: boolean;
placeholder?: string;
+ menuPortalTarget?: HTMLElement | null;
}
const baseClass = "dropdown-wrapper";
@@ -71,6 +72,7 @@ const DropdownWrapper = ({
isSearchable,
isDisabled = false,
placeholder,
+ menuPortalTarget,
}: IDropdownWrapper) => {
const wrapperClassNames = classnames(baseClass, className);
@@ -317,7 +319,6 @@ const DropdownWrapper = ({
styles={customStyles}
options={options}
components={{
- // Control: CustomControl,
Option: CustomOption,
DropdownIndicator: CustomDropdownIndicator,
IndicatorSeparator: () => null,
@@ -325,7 +326,9 @@ const DropdownWrapper = ({
value={getCurrentValue()}
onChange={handleChange}
isDisabled={isDisabled}
- menuPortalTarget={document.body}
+ menuPortalTarget={
+ menuPortalTarget === undefined ? document.body : menuPortalTarget
+ }
noOptionsMessage={() => "No results found"}
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
placeholder={placeholder}
diff --git a/frontend/components/forms/fields/DropdownWrapper/_styles.scss b/frontend/components/forms/fields/DropdownWrapper/_styles.scss
index 8bd7de893720..2a92f5ced65a 100644
--- a/frontend/components/forms/fields/DropdownWrapper/_styles.scss
+++ b/frontend/components/forms/fields/DropdownWrapper/_styles.scss
@@ -1,5 +1,7 @@
-// react-select's customStyles styles the internal components
-// and not external elements like labels
+// react-select's styles prop customizes the styling of
+// the internal components and not external elements like labels
+// See customStyles in DropdownWrappr.tsx
+// https://react-select.com/styles
.dropdown-wrapper {
&__label {
&--error {
diff --git a/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx b/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
index 4ddd800a9939..c0236b5f8899 100644
--- a/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/SelectRoleForm/SelectRoleForm.tsx
@@ -12,7 +12,6 @@ interface ISelectRoleFormProps {
currentTeam?: ITeam;
teams: ITeam[];
onFormChange: (teams: ITeam[]) => void;
- label: string | string[];
isApiOnly?: boolean;
}
diff --git a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
index 0e74b29c58d9..c89dc37dcccd 100644
--- a/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/UserForm/UserForm.tsx
@@ -370,7 +370,6 @@ const UserForm = ({
>
) : (