diff --git a/CHANGELOG.md b/CHANGELOG.md index ec554cea35..bf3831e84a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,52 @@ +## Fleet 4.60.0 (Nov 27, 2024) + +### Endpoint operations +- Added support for labels_include_any to gitops. +- Added major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation). +- Added activity item for `fleetd` enrollment with host serial and display name. +- Added capability for Fleet to serve YARA rules to agents over HTTPS authenticated via node key (requires osquery 5.14+). +- Added a query to allow users to turn on/off automations while being transparent of the current log destination. +- Updated UI to allow users to view scripts (from both the scripts page and host details page) without downloading them. +- Updated activity feed to generate an activity when activity automations are enabled, edited, or disabled. +- Cancelled pending script executions when a script is edited or deleted. + +### Device management (MDM) +- Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy. +- Added info banner for cloud customers to help with their windows autoenrollment setup. +- Added DB support for "include any" label profile deployment. +- Added support for "include any" label/profile relationships to the profile reconciliation machinery. +- Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint. +- Added indicator of how fresh a software title's host and version counts are on the title's details page. +- Added UI for allowing users to install custom profiles on hosts that include any of the defined labels. +- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. +- Added support for deb packages compressed with zstd. + +### Vulnerability management +- Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`). + +### Bug fixes and improvements +- Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required. +- Improved performance of adding and removing profiles to large teams by an order of magnitude. +- Disabled accessibility via keyboard for forms that are disabled via a slider. +- Updated software batch endpoint status code from 200 (OK) to 202 (Accepted). +- Updated a package used for testing (msw) to improve security. +- Updated to reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04. +- Updated GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there are more than 1 ABM tokens in Fleet. +- Dismissed error flash on the my device page when navigating to another URL. +- Modified the Fleet setup experience feature to not run if there is no software or script configured for the setup experience. +- Set a more accurate minimum height for the Add hosts > ChromeOS > Policy for extension field, avoiding a scrollbar. +- Added UI prompt for user to reenter the password if SCEP/NDES url or username has changed. +- Updated ABM public key to download as as PEM format instead of CRT. +- Fixed issue with uploading macOS software packages that do not have a top level `Distribution.xml`, but do have a top level `PackageInfo.xml`. For example, Okta Verify.app. +- Fixed some cases where Fleet Maintained Apps generated incorrect uninstall scripts. +- Fixed a bug where a device that was removed from ABM and then added back wouldn't properly re-enroll in Fleet MDM. +- Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. +- Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica. +- Fixed the MDM configuration profiles deployment when based on excluded labels. +- Fixed gitops path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere). +- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM IdP integration was enabled. +- Fixed a bug where users would be allowed to attempt an install of an App Store app on a host that was not MDM enrolled. + ## Fleet 4.59.1 (Nov 18, 2024) ### Bug fixes diff --git a/changes/14899-yara-rules b/changes/14899-yara-rules deleted file mode 100644 index 2c92188cfc..0000000000 --- a/changes/14899-yara-rules +++ /dev/null @@ -1 +0,0 @@ -* Added capability for Fleet to serve yara rules to agents over HTTPS authenticated via node key (requires osquery 5.14+). \ No newline at end of file diff --git a/changes/20595-improve-memory-usage-software-installers b/changes/20595-improve-memory-usage-software-installers deleted file mode 100644 index 7e15f3b935..0000000000 --- a/changes/20595-improve-memory-usage-software-installers +++ /dev/null @@ -1 +0,0 @@ -* Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required. diff --git a/changes/21338-scope-profile-pending-rebuild b/changes/21338-scope-profile-pending-rebuild deleted file mode 100644 index 59e4883955..0000000000 --- a/changes/21338-scope-profile-pending-rebuild +++ /dev/null @@ -1 +0,0 @@ -- Speed up adding and removing profiles to large teams by an order of magnitude diff --git a/changes/21633-windows-auto-enrollment-info-banner b/changes/21633-windows-auto-enrollment-info-banner deleted file mode 100644 index 86cdfafdaf..0000000000 --- a/changes/21633-windows-auto-enrollment-info-banner +++ /dev/null @@ -1 +0,0 @@ -- add info banner for cloud customers to help with their windows autoenrollment setup diff --git a/changes/21709-activities-automation-activity b/changes/21709-activities-automation-activity deleted file mode 100644 index bc47a6e273..0000000000 --- a/changes/21709-activities-automation-activity +++ /dev/null @@ -1 +0,0 @@ -* Generate an activity when activity automations are enabled, edited, or disabled. diff --git a/changes/21888-dequeue-pending-scripts b/changes/21888-dequeue-pending-scripts deleted file mode 100644 index 3852ee09c3..0000000000 --- a/changes/21888-dequeue-pending-scripts +++ /dev/null @@ -1 +0,0 @@ -* Cancelled pending script executions when a script is edited or deleted. diff --git a/changes/21908-replace-mozilla-pkcs7 b/changes/21908-replace-mozilla-pkcs7 new file mode 100644 index 0000000000..65fc8d79bf --- /dev/null +++ b/changes/21908-replace-mozilla-pkcs7 @@ -0,0 +1 @@ +* Replaced the internal use of the deprecated `go.mozilla.org/pkcs7` package with the maintained fork `github.com/smallstep/pkcs7`. diff --git a/changes/22162-exclude-labels-fix-default-behavior b/changes/22162-exclude-labels-fix-default-behavior deleted file mode 100644 index 41524c8c03..0000000000 --- a/changes/22162-exclude-labels-fix-default-behavior +++ /dev/null @@ -1 +0,0 @@ -* Fixed the MDM configuration profiles deployment when based on excluded labels - prior to this fix, hosts were considered "not a member" of the label by default, even if they had not yet returned results for the excluded labels. The fix checks the label's creation time vs the host's last reported label results timestamp to prevent deploying a configuration profile if it does not yet know if the host is a member or not of those labels. diff --git a/changes/22187-gitops-software-relative-paths b/changes/22187-gitops-software-relative-paths deleted file mode 100644 index 8f1ce8f480..0000000000 --- a/changes/22187-gitops-software-relative-paths +++ /dev/null @@ -1 +0,0 @@ -* GitOps: Fixed path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere). \ No newline at end of file diff --git a/changes/22224-query-log-destinations b/changes/22224-query-log-destinations deleted file mode 100644 index b6172a331b..0000000000 --- a/changes/22224-query-log-destinations +++ /dev/null @@ -1 +0,0 @@ -- Creating a query allow users to turn on/off automations while being transparent of the current log destination diff --git a/changes/22269-software-title-updated-at b/changes/22269-software-title-updated-at deleted file mode 100644 index dfc3f12769..0000000000 --- a/changes/22269-software-title-updated-at +++ /dev/null @@ -1 +0,0 @@ -* Added indicator of how fresh a software title's host and version counts are on the title's details page diff --git a/changes/22359-gitops-mult-abm b/changes/22359-gitops-mult-abm deleted file mode 100644 index b7a7801edb..0000000000 --- a/changes/22359-gitops-mult-abm +++ /dev/null @@ -1,2 +0,0 @@ -- Updates GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there - are more than 1 ABM tokens in Fleet. \ No newline at end of file diff --git a/changes/22361-os-update-ade-sso b/changes/22361-os-update-ade-sso deleted file mode 100644 index 40221866fb..0000000000 --- a/changes/22361-os-update-ade-sso +++ /dev/null @@ -1,2 +0,0 @@ -- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM - IdP integration was enabled. diff --git a/changes/22437-linux-lock-black-screen b/changes/22437-linux-lock-black-screen deleted file mode 100644 index edfd4dc8d4..0000000000 --- a/changes/22437-linux-lock-black-screen +++ /dev/null @@ -1 +0,0 @@ -- Reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04 diff --git a/changes/22446-scripts-modal b/changes/22446-scripts-modal deleted file mode 100644 index 1e06aea931..0000000000 --- a/changes/22446-scripts-modal +++ /dev/null @@ -1 +0,0 @@ -- Users can view scripts in the UI (from both the scripts page and host details page) without downloading them diff --git a/changes/22575-ui-for-include-any-labels b/changes/22575-ui-for-include-any-labels deleted file mode 100644 index 5f66f8396b..0000000000 --- a/changes/22575-ui-for-include-any-labels +++ /dev/null @@ -1,2 +0,0 @@ -- add UI for allowing users to install custom profiles on hosts that include any of the defined -labels diff --git a/changes/22576-labels-include-any-gitops b/changes/22576-labels-include-any-gitops deleted file mode 100644 index 228171c7d1..0000000000 --- a/changes/22576-labels-include-any-gitops +++ /dev/null @@ -1 +0,0 @@ -- Add support for labels_include_any to gitops diff --git a/changes/22578-db-schema b/changes/22578-db-schema deleted file mode 100644 index 281c14a6b9..0000000000 --- a/changes/22578-db-schema +++ /dev/null @@ -1 +0,0 @@ -- Adds DB support for "include any" label profile deployment \ No newline at end of file diff --git a/changes/22581-cron-updates b/changes/22581-cron-updates deleted file mode 100644 index f228460a04..0000000000 --- a/changes/22581-cron-updates +++ /dev/null @@ -1 +0,0 @@ -- Adds support for "include any" label/profile relationships to the profile reconciliation machinery. \ No newline at end of file diff --git a/changes/22606-keyboard-accessiblity b/changes/22606-keyboard-accessiblity deleted file mode 100644 index 6f863e248a..0000000000 --- a/changes/22606-keyboard-accessiblity +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation) \ No newline at end of file diff --git a/changes/22702-linux-encryption-frontend b/changes/22702-linux-encryption-frontend deleted file mode 100644 index a35d242375..0000000000 --- a/changes/22702-linux-encryption-frontend +++ /dev/null @@ -1 +0,0 @@ -- Added UI features supporting disk encryption for Ubuntu and Fedora Linux. diff --git a/changes/22773-fma-uninstall-fix b/changes/22773-fma-uninstall-fix deleted file mode 100644 index 74c4390533..0000000000 --- a/changes/22773-fma-uninstall-fix +++ /dev/null @@ -1 +0,0 @@ -- Fix some cases where Fleet Maintained Apps generated incorrect uninstall scripts diff --git a/changes/22810-fleetd-enroll-activity b/changes/22810-fleetd-enroll-activity deleted file mode 100644 index b9b9380a05..0000000000 --- a/changes/22810-fleetd-enroll-activity +++ /dev/null @@ -1 +0,0 @@ -Added activity item for fleetd enrollment with host serial and display name. diff --git a/changes/22891-zstd-deb-packages b/changes/22891-zstd-deb-packages deleted file mode 100644 index f523dd6272..0000000000 --- a/changes/22891-zstd-deb-packages +++ /dev/null @@ -1 +0,0 @@ -- Add support for deb packages compressed with zstd diff --git a/changes/22985-disable-forms-keyboard-access b/changes/22985-disable-forms-keyboard-access deleted file mode 100644 index 2e90b69dc5..0000000000 --- a/changes/22985-disable-forms-keyboard-access +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Disable accessibility via keyboard for forms that are disabled via a slider diff --git a/changes/23016-add-chrome-host-text-area-height b/changes/23016-add-chrome-host-text-area-height deleted file mode 100644 index 7616f4bfa0..0000000000 --- a/changes/23016-add-chrome-host-text-area-height +++ /dev/null @@ -1,2 +0,0 @@ -* Set a more elegant minimum height for the Add hosts > ChromeOS > Policy for extension field, -avoiding a scrollbar. diff --git a/changes/23021-abm-cert-pem b/changes/23021-abm-cert-pem deleted file mode 100644 index c1890e07bb..0000000000 --- a/changes/23021-abm-cert-pem +++ /dev/null @@ -1 +0,0 @@ -- Download ABM public key as PEM format instead of CRT diff --git a/changes/23078-allow-skipping-vuln-details b/changes/23078-allow-skipping-vuln-details deleted file mode 100644 index 7a29933976..0000000000 --- a/changes/23078-allow-skipping-vuln-details +++ /dev/null @@ -1 +0,0 @@ -* Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`) \ No newline at end of file diff --git a/changes/23128-update-mock-service-worker-package-for-secutiy b/changes/23128-update-mock-service-worker-package-for-secutiy deleted file mode 100644 index aa9a3e47af..0000000000 --- a/changes/23128-update-mock-service-worker-package-for-secutiy +++ /dev/null @@ -1 +0,0 @@ -- update a package used for testing (msw) to improve security diff --git a/changes/23200-ade-enroll b/changes/23200-ade-enroll deleted file mode 100644 index 6a6c597bf4..0000000000 --- a/changes/23200-ade-enroll +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes a bug where a device that was removed from ABM and then added back wouldn't properly - re-enroll in Fleet MDM \ No newline at end of file diff --git a/changes/23213-okta-verify b/changes/23213-okta-verify deleted file mode 100644 index 6fd38a9e47..0000000000 --- a/changes/23213-okta-verify +++ /dev/null @@ -1 +0,0 @@ -Fixed issue with uploading macOS software packages that do not have a top level Distribution.xml, but do have a top level PackageInfo.xml. For example, Okta Verify.app diff --git a/changes/23247-vpp-app-install b/changes/23247-vpp-app-install deleted file mode 100644 index 97a62eb9df..0000000000 --- a/changes/23247-vpp-app-install +++ /dev/null @@ -1,2 +0,0 @@ -- Fixes a bug where users would be allowed to attempt an install of an App Store app on a host that - was not MDM enrolled. \ No newline at end of file diff --git a/changes/23462-show-windows-mdm-wstep-options b/changes/23462-show-windows-mdm-wstep-options new file mode 100644 index 0000000000..8df6b93139 --- /dev/null +++ b/changes/23462-show-windows-mdm-wstep-options @@ -0,0 +1 @@ +- Display Windows MDM WSTEP flags in `fleet --help`. diff --git a/changes/23492-software-batch-status-code b/changes/23492-software-batch-status-code deleted file mode 100644 index 9ab51770d9..0000000000 --- a/changes/23492-software-batch-status-code +++ /dev/null @@ -1 +0,0 @@ -* Updated software batch endpoint status code from 200 (OK) to 202 (Accepted) \ No newline at end of file diff --git a/changes/23525-ndes-errors b/changes/23525-ndes-errors deleted file mode 100644 index 409723e809..0000000000 --- a/changes/23525-ndes-errors +++ /dev/null @@ -1 +0,0 @@ -Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy. diff --git a/changes/23540-pe-sfx b/changes/23540-pe-sfx deleted file mode 100644 index 63c241a8be..0000000000 --- a/changes/23540-pe-sfx +++ /dev/null @@ -1 +0,0 @@ -Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera. diff --git a/changes/23597-fix-create-update-label-returns-outdated-info b/changes/23597-fix-create-update-label-returns-outdated-info deleted file mode 100644 index 3a5e26e5aa..0000000000 --- a/changes/23597-fix-create-update-label-returns-outdated-info +++ /dev/null @@ -1 +0,0 @@ -* Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica. diff --git a/changes/23651-reenter-password b/changes/23651-reenter-password deleted file mode 100644 index b3fc7df44d..0000000000 --- a/changes/23651-reenter-password +++ /dev/null @@ -1 +0,0 @@ -- Fleet UI: Prompt user to reenter the password if SCEP/NDES url or username has changed diff --git a/changes/23669-dismiss-error-flash-on-url-change-dup b/changes/23669-dismiss-error-flash-on-url-change-dup deleted file mode 100644 index 125774f81f..0000000000 --- a/changes/23669-dismiss-error-flash-on-url-change-dup +++ /dev/null @@ -1 +0,0 @@ -* Dismiss error flash on the my device page when navigating to another URL. \ No newline at end of file diff --git a/changes/24024-no-setup-exp b/changes/24024-no-setup-exp deleted file mode 100644 index 44ab42bcf0..0000000000 --- a/changes/24024-no-setup-exp +++ /dev/null @@ -1,2 +0,0 @@ -- Modifies the Fleet setup experience feature to not run if there is no software or script - configured for the setup experience. \ No newline at end of file diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml index f9b80ed1e3..aeb5a838e0 100644 --- a/charts/fleet/Chart.yaml +++ b/charts/fleet/Chart.yaml @@ -4,11 +4,11 @@ name: fleet keywords: - fleet - osquery -version: v6.2.2 +version: v6.2.3 home: https://github.com/fleetdm/fleet sources: - https://github.com/fleetdm/fleet.git -appVersion: v4.59.1 +appVersion: v4.60.0 dependencies: - name: mysql condition: mysql.enabled diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml index 7e1c7f7916..231c8bb22b 100644 --- a/charts/fleet/values.yaml +++ b/charts/fleet/values.yaml @@ -3,7 +3,7 @@ hostName: fleet.localhost replicas: 3 # The number of Fleet instances to deploy imageRepository: fleetdm/fleet -imageTag: v4.59.1 # Version of Fleet to deploy +imageTag: v4.60.0 # Version of Fleet to deploy podAnnotations: {} # Additional annotations to add to the Fleet pod serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account resources: diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index d3947370a2..8049b47441 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -37,7 +37,6 @@ export interface IModalProps { * */ disableClosingModal?: boolean; className?: string; - actionsFooter?: JSX.Element; } const Modal = ({ @@ -51,7 +50,6 @@ const Modal = ({ isContentDisabled = false, disableClosingModal = false, className, - actionsFooter, }: IModalProps): JSX.Element => { useEffect(() => { const closeWithEscapeKey = (e: KeyboardEvent) => { @@ -127,9 +125,6 @@ const Modal = ({ )}
{children}
- {actionsFooter && ( -
{actionsFooter}
- )} ); diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index ef41106731..747f6e58f2 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -25,17 +25,9 @@ &__content-wrapper { margin-top: $pad-large; font-size: $x-small; - // New pattern of max height modals pushed to 4.61 with PR #24019 - overflow: visible; .input-field { width: 100%; - - &::placeholder { - font-size: $x-small; - font-style: italic; - line-height: 24px; - } } form .modal-cta-wrap, @@ -142,6 +134,11 @@ } } +.modal-scrollable-content { + overflow-y: auto; + max-height: 705px; +} + .modal-cta-wrap { align-self: flex-end; display: flex; diff --git a/frontend/components/ModalFooter/ModalFooter.stories.tsx b/frontend/components/ModalFooter/ModalFooter.stories.tsx new file mode 100644 index 0000000000..802f76f9f0 --- /dev/null +++ b/frontend/components/ModalFooter/ModalFooter.stories.tsx @@ -0,0 +1,63 @@ +/* eslint-disable no-alert */ +import React from "react"; +import { Meta, Story } from "@storybook/react"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; +import ActionsDropdown from "components/ActionsDropdown"; +import ModalFooter from "./ModalFooter"; + +export default { + title: "Components/ModalFooter", + component: ModalFooter, +} as Meta; + +const Template: Story = (args) => ( + } {...args} /> +); + +export const Default = Template.bind({}); +Default.args = { + primaryButtons: ( + <> + alert(`Selected action: ${value}`)} + placeholder="More actions" + isSearchable={false} + options={[ + { value: "action1", label: "Action 1" }, + { value: "action2", label: "Action 2" }, + ]} + menuPlacement="top" + /> + + + ), + secondaryButtons: ( + <> + + + + ), + isTopScrolling: false, +}; + +export const WithTopScrolling = Template.bind({}); +WithTopScrolling.args = { + ...Default.args, + isTopScrolling: true, +}; + +export const WithoutSecondaryButtons = Template.bind({}); +WithoutSecondaryButtons.args = { + primaryButtons: Default.args.primaryButtons, + secondaryButtons: undefined, + isTopScrolling: false, +}; diff --git a/frontend/components/ModalFooter/ModalFooter.tsx b/frontend/components/ModalFooter/ModalFooter.tsx new file mode 100644 index 0000000000..4b24cc6db0 --- /dev/null +++ b/frontend/components/ModalFooter/ModalFooter.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import classnames from "classnames"; +import { COLORS } from "styles/var/colors"; + +const baseClass = "modal-footer"; + +interface IModalFooterProps { + primaryButtons: JSX.Element; + secondaryButtons?: JSX.Element; + className?: string; + /** Renders a line above action buttons to indicate scrollability */ + isTopScrolling?: boolean; +} + +const ModalFooter = ({ + primaryButtons, + secondaryButtons, + className, + isTopScrolling = false, +}: IModalFooterProps): JSX.Element => { + const classes = classnames(className, `${baseClass}__content-wrapper`); + + return ( +
+
+ {primaryButtons} +
+ {secondaryButtons && ( +
+ {secondaryButtons} +
+ )} +
+ ); +}; + +export default ModalFooter; diff --git a/frontend/components/ModalFooter/_styles.scss b/frontend/components/ModalFooter/_styles.scss new file mode 100644 index 0000000000..679551e1d8 --- /dev/null +++ b/frontend/components/ModalFooter/_styles.scss @@ -0,0 +1,27 @@ +.modal-footer { + &__content-wrapper { + align-self: flex-end; + display: flex; + flex-direction: row-reverse; + padding-top: $pad-medium; + justify-content: space-between; + } + + // Styles both primary-actions and secondary-actions + &__primary-buttons-wrapper, + &__secondary-buttons_wrapper { + display: flex; + justify-content: space-between; + gap: $pad-medium; + align-items: center; + } + + // Align primary actions right if no secondary actions + > :last-child { + margin-left: auto; + } + + .button__text-icon { + padding: 11px; + } +} diff --git a/frontend/components/ModalFooter/index.ts b/frontend/components/ModalFooter/index.ts new file mode 100644 index 0000000000..6a36137070 --- /dev/null +++ b/frontend/components/ModalFooter/index.ts @@ -0,0 +1 @@ +export { default } from "./ModalFooter"; diff --git a/frontend/components/TeamsDropdown/TeamsDropdown.tsx b/frontend/components/TeamsDropdown/TeamsDropdown.tsx index e38b1c1b13..bc59c6fb0c 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 7fcd24ad33..23bf82f070 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 0000000000..ee0d223c58 --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.stories.tsx @@ -0,0 +1,62 @@ +// stories/DropdownWrapper.stories.tsx + +import React from "react"; +import { Meta, Story } from "@storybook/react"; +import DropdownWrapper, { + IDropdownWrapper, + CustomOptionType, +} from "./DropdownWrapper"; + +// Define metadata for the story +export default { + 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: sampleOptions, + name: "dropdown-example", + label: "Select an option", +}; + +// Disabled story +export const Disabled = Template.bind({}); +Disabled.args = { + ...Default.args, + isDisabled: true, +}; + +// 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", +}; 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 0000000000..2715876f8b --- /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 new file mode 100644 index 0000000000..51c5c3cd69 --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/DropdownWrapper.tsx @@ -0,0 +1,340 @@ +/** + * 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, option helptext, option tooltip + * Other: label text, dropdown help text, dropdown error + */ + +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; + placeholder?: string; + menuPortalTarget?: HTMLElement | null; +} + +const baseClass = "dropdown-wrapper"; + +const DropdownWrapper = ({ + options, + value, + onChange, + name, + className, + labelClassname, + error, + label, + helpText, + isSearchable, + isDisabled = false, + placeholder, + menuPortalTarget, +}: IDropdownWrapper) => { + 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 + noOptionsMessage: (provided) => ({ + ...provided, + textAlign: "left", + fontSize: "14px", + padding: "10px 8px", + }), + }; + + const renderLabel = () => { + const labelWrapperClasses = classnames( + `${baseClass}__label`, + labelClassname, + { + [`${baseClass}__label--error`]: !!error, + [`${baseClass}__label--disabled`]: isDisabled, + } + ); + + if (!label) { + return ""; + } + + return ( + + ); + }; + + return ( + + + classNamePrefix="react-select" + isSearchable={isSearchable} + styles={customStyles} + options={options} + components={{ + Option: CustomOption, + DropdownIndicator: CustomDropdownIndicator, + IndicatorSeparator: () => null, + }} + value={getCurrentValue()} + onChange={handleChange} + isDisabled={isDisabled} + menuPortalTarget={ + menuPortalTarget === undefined ? document.body : menuPortalTarget + } + noOptionsMessage={() => "No results found"} + tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility + placeholder={placeholder} + /> + + ); +}; + +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 0000000000..2a92f5ced6 --- /dev/null +++ b/frontend/components/forms/fields/DropdownWrapper/_styles.scss @@ -0,0 +1,14 @@ +// react-select's