From 7b7fa2fcd114059b1a709003bfad4352f8e319e1 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 26 Feb 2025 13:47:28 -0500 Subject: [PATCH 01/15] Fleet UI: Component fixes (styling bugs and code cleanup) (#26149) --- changes/26136-user-table-cleanup | 1 + .../CustomLink/CustomLink.stories.tsx | 79 ++++++++ frontend/components/CustomLink/CustomLink.tsx | 19 +- frontend/components/CustomLink/_styles.scss | 27 ++- .../LicenseExpirationBanner.tsx | 3 +- .../AppleBMRenewalMessage.tsx | 3 +- .../AppleBMTermsMessage.tsx | 3 +- .../ApplePNCertRenewalMessage.tsx | 3 +- .../VppRenewalMessage/VppRenewalMessage.tsx | 3 +- .../PremiumFeatureIconWithTooltip.tsx | 59 ------ ...PremiumFeatureIconWithToooltip.stories.tsx | 14 -- .../_styles.scss | 5 - .../PremiumFeatureIconWithTooltip/index.ts | 1 - .../DataTable/ActionButton/ActionButton.tsx | 12 +- .../TableContainer/DataTable/DataTable.tsx | 2 - .../TooltipTruncatedTextCell.tsx | 10 +- .../TooltipTruncatedTextCell/_styles.scss | 19 ++ .../AutoSizeInputField.stories.tsx | 18 +- .../AutoSizeInputField/AutoSizeInputField.tsx | 2 +- .../DashboardPage/cards/MDM/_styles.scss | 11 -- .../DashboardPage/cards/Munki/_styles.scss | 4 - .../DashboardPage/cards/Software/_styles.scss | 1 - .../BootstrapPackageTable/_styles.scss | 4 - .../SetupAssistantProfileUploader.tsx | 2 - .../FleetMaintainedAppDetailsPage.tsx | 1 + .../SoftwareTitleDetailsTable/_styles.scss | 7 - .../SoftwareTitles/SoftwareTable/_styles.scss | 5 - .../SoftwareVulnerabilitiesTable/_styles.scss | 4 - .../UsersPage/UsersPageTableConfig.tsx | 86 ++++---- .../TeamDetailsWrapper/UsersPage/_styles.scss | 14 +- .../TeamDetailsWrapper/_styles.scss | 6 + .../admin/TeamManagementPage/_styles.scss | 12 +- .../admin/UserManagementPage/_styles.scss | 184 +++++++++--------- .../UsersTable/UsersTableConfig.tsx | 60 ++---- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 5 +- .../DeviceUserBanners/DeviceUserBanners.tsx | 3 +- .../HostDetailsBanners/HostDetailsBanners.tsx | 3 +- .../OSSettingsErrorCell.tsx | 2 +- 38 files changed, 301 insertions(+), 396 deletions(-) create mode 100644 changes/26136-user-table-cleanup delete mode 100644 frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx delete mode 100644 frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithToooltip.stories.tsx delete mode 100644 frontend/components/PremiumFeatureIconWithTooltip/_styles.scss delete mode 100644 frontend/components/PremiumFeatureIconWithTooltip/index.ts delete mode 100644 frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsTable/_styles.scss diff --git a/changes/26136-user-table-cleanup b/changes/26136-user-table-cleanup new file mode 100644 index 0000000000..74824d55b1 --- /dev/null +++ b/changes/26136-user-table-cleanup @@ -0,0 +1 @@ +- Fleet UI: Constistent behavior for table overflow and not hiding badges when user names overflow table cell diff --git a/frontend/components/CustomLink/CustomLink.stories.tsx b/frontend/components/CustomLink/CustomLink.stories.tsx index 5e8600c15a..cf4b3c2800 100644 --- a/frontend/components/CustomLink/CustomLink.stories.tsx +++ b/frontend/components/CustomLink/CustomLink.stories.tsx @@ -1,5 +1,8 @@ +import React from "react"; import { Meta, StoryObj } from "@storybook/react"; +import InfoBanner from "components/InfoBanner"; +import TooltipWrapper from "components/TooltipWrapper"; import CustomLink from "."; const meta: Meta = { @@ -24,3 +27,79 @@ export const ExternalLink: Story = { newTab: true, }, }; + +export const Multiline: Story = { + render: (args) => ( +
+ Here's a CustomLink in a that might be split up across two lines{" "} + +
+ ), + args: { + url: "https://www.google.com", + text: + "This is a custom link that has multiple words that might span multiple lines and the icon should stick with the last word onto the new line", + multiline: true, + newTab: true, + }, +}; + +export const TooltipVariant: Story = { + render: (args) => ( + + Tip content with a custom link + + } + > + Hover to see custom link in tooltip{" "} + + ), + args: { + url: "https://www.google.com", + text: "Tooltip link", + variant: "tooltip-link", + newTab: true, + }, +}; + +export const BannerVariant: Story = { + render: (args) => ( + + Here's a CustomLink in a banner + + ), + args: { + url: "https://www.google.com", + text: "Banner link", + variant: "banner-link", + newTab: true, + }, +}; + +export const FlashMessageVariant: Story = { + args: { + url: "https://www.google.com", + text: "Flash message link", + variant: "flash-message-link", + }, +}; + +export const DisabledKeyboardNav: Story = { + render: (args) => ( + <> + Here, you can't tab to this link even if you wanted to which is + useful when within a disabled component. + + ), + args: { + url: "https://www.google.com", + text: "Disabled Keyboard Navigation", + disableKeyboardNavigation: true, + }, +}; diff --git a/frontend/components/CustomLink/CustomLink.tsx b/frontend/components/CustomLink/CustomLink.tsx index b3b45eaccc..f99d02f37d 100644 --- a/frontend/components/CustomLink/CustomLink.tsx +++ b/frontend/components/CustomLink/CustomLink.tsx @@ -14,22 +14,14 @@ interface ICustomLinkProps { newTab?: boolean; /** Icon wraps on new line with last word */ multiline?: boolean; - // TODO: Refactor to use variant - iconColor?: Colors; - // TODO: Refactor to use variant - color?: "core-fleet-blue" | "core-fleet-black" | "core-fleet-white"; /** Restricts access via keyboard when CustomLink is part of disabled UI */ disableKeyboardNavigation?: boolean; /** * Changes the appearance of the link. * * @default "default" - * - * TODO: - * Longterm: refactor 14 instances away from iconColor/color combo, which - * usually are identical and repetitive, toward variants e.g. "banner-link" */ - variant?: "tooltip-link" | "default" | "flash-message-link"; + variant?: "tooltip-link" | "banner-link" | "flash-message-link" | "default"; } const baseClass = "custom-link"; @@ -40,8 +32,6 @@ const CustomLink = ({ className, newTab = false, multiline = false, - iconColor = "core-fleet-blue", - color = "core-fleet-blue", disableKeyboardNavigation = false, variant = "default", }: ICustomLinkProps): JSX.Element => { @@ -50,15 +40,16 @@ const CustomLink = ({ case "tooltip-link": case "flash-message-link": return "core-fleet-white"; + case "banner-link": + return "core-fleet-black"; default: - return iconColor; + return "core-fleet-blue"; } }; const customLinkClass = classnames(baseClass, className, { - [`${baseClass}--black`]: color === "core-fleet-black", - [`${baseClass}--white`]: color === "core-fleet-white", [`${baseClass}--${variant}`]: variant !== "default", + [`${baseClass}--multiline`]: multiline, }); const target = newTab ? "_blank" : ""; diff --git a/frontend/components/CustomLink/_styles.scss b/frontend/components/CustomLink/_styles.scss index 8d050125ae..60dc80cb71 100644 --- a/frontend/components/CustomLink/_styles.scss +++ b/frontend/components/CustomLink/_styles.scss @@ -1,7 +1,10 @@ .custom-link { - display: inline-flex; - align-items: center; - gap: $pad-xsmall; + // Changing display will break multiline links + &:not(.custom-link--multiline) { + display: inline-flex; + align-items: center; + gap: $pad-xsmall; + } &__no-wrap { white-space: nowrap; @@ -10,18 +13,14 @@ } } - // Legacy - &--black { - color: $core-fleet-black; - } - - &--white { - color: $core-fleet-white; - } - // Variants + &--tooltip-link, + &--banner-link, + &--flash-messsage-link { + color: inherit; // Overrides fleet blue link color with parent color + } + &--tooltip-link { - color: $core-fleet-white; - font-size: $xx-small; + font-size: inherit; // Overrides link default font size with parent tooltip font size } } diff --git a/frontend/components/LicenseExpirationBanner/LicenseExpirationBanner.tsx b/frontend/components/LicenseExpirationBanner/LicenseExpirationBanner.tsx index 014283fa36..c1333b43b3 100644 --- a/frontend/components/LicenseExpirationBanner/LicenseExpirationBanner.tsx +++ b/frontend/components/LicenseExpirationBanner/LicenseExpirationBanner.tsx @@ -15,8 +15,7 @@ const LicenseExpirationBanner = (): JSX.Element => { url="https://fleetdm.com/learn-more-about/downgrading" text="Downgrade or renew" newTab - iconColor="core-fleet-black" - color="core-fleet-black" + variant="banner-link" /> } > diff --git a/frontend/components/MDM/AppleBMRenewalMessage/AppleBMRenewalMessage.tsx b/frontend/components/MDM/AppleBMRenewalMessage/AppleBMRenewalMessage.tsx index c64031d51f..d8a6a7cf15 100644 --- a/frontend/components/MDM/AppleBMRenewalMessage/AppleBMRenewalMessage.tsx +++ b/frontend/components/MDM/AppleBMRenewalMessage/AppleBMRenewalMessage.tsx @@ -19,8 +19,7 @@ const AppleBMRenewalMessage = ({ expired }: IAppleBMRenewalMessageProps) => { url="/settings/integrations/mdm/abm" text="Renew ABM" className={`${baseClass}`} - color="core-fleet-black" - iconColor="core-fleet-black" + variant="banner-link" /> } > diff --git a/frontend/components/MDM/AppleBMTermsMessage/AppleBMTermsMessage.tsx b/frontend/components/MDM/AppleBMTermsMessage/AppleBMTermsMessage.tsx index 6fcff4be67..ce59db8432 100644 --- a/frontend/components/MDM/AppleBMTermsMessage/AppleBMTermsMessage.tsx +++ b/frontend/components/MDM/AppleBMTermsMessage/AppleBMTermsMessage.tsx @@ -16,8 +16,7 @@ const AppleBMTermsMessage = () => { text="Go to ABM" className={`${baseClass}__new-tab`} newTab - color="core-fleet-black" - iconColor="core-fleet-black" + variant="banner-link" /> } > diff --git a/frontend/components/MDM/ApplePNCertRenewalMessage/ApplePNCertRenewalMessage.tsx b/frontend/components/MDM/ApplePNCertRenewalMessage/ApplePNCertRenewalMessage.tsx index 824468ea61..7e790f4d96 100644 --- a/frontend/components/MDM/ApplePNCertRenewalMessage/ApplePNCertRenewalMessage.tsx +++ b/frontend/components/MDM/ApplePNCertRenewalMessage/ApplePNCertRenewalMessage.tsx @@ -20,8 +20,7 @@ const ApplePNCertRenewalMessage = ({ expired }: IApplePNCertRenewalMessage) => { text="Renew APNs" className={`${baseClass}__new-tab`} newTab - color="core-fleet-black" - iconColor="core-fleet-black" + variant="banner-link" /> } > diff --git a/frontend/components/MainContent/banners/VppRenewalMessage/VppRenewalMessage.tsx b/frontend/components/MainContent/banners/VppRenewalMessage/VppRenewalMessage.tsx index 8980f485f2..b4498002df 100644 --- a/frontend/components/MainContent/banners/VppRenewalMessage/VppRenewalMessage.tsx +++ b/frontend/components/MainContent/banners/VppRenewalMessage/VppRenewalMessage.tsx @@ -19,8 +19,7 @@ const VppRenewalMessage = ({ expired }: IVppRenewalMessageProps) => { url="/settings/integrations/mdm/vpp" text="Renew VPP" className={`${baseClass}`} - color="core-fleet-black" - iconColor="core-fleet-black" + variant="banner-link" /> } > diff --git a/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx b/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx deleted file mode 100644 index 12fa73b863..0000000000 --- a/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithTooltip.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import CustomLink from "components/CustomLink"; -import Icon from "components/Icon"; -import { uniqueId } from "lodash"; -import React from "react"; -import ReactTooltip, { Place } from "react-tooltip"; -import { COLORS } from "styles/var/colors"; - -interface IPremiumFeatureIconWithTooltip { - tooltipPlace?: Place; - tooltipDelayHide?: number; - tooltipPositionOverrides?: { - leftAdj?: number; - topAdj?: number; - }; -} -const PremiumFeatureIconWithTooltip = ({ - tooltipPlace, - tooltipDelayHide = 100, - tooltipPositionOverrides, -}: IPremiumFeatureIconWithTooltip) => { - const [leftAdj, topAdj] = [ - tooltipPositionOverrides?.leftAdj ?? 0, - tooltipPositionOverrides?.topAdj ?? 0, - ]; - const tipId = uniqueId(); - return ( - - - - - { - return { - left: pos.left + leftAdj, - top: pos.top + topAdj, - }; - }} - > - {`This is a Fleet Premium feature. `} - - - - ); -}; - -export default PremiumFeatureIconWithTooltip; diff --git a/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithToooltip.stories.tsx b/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithToooltip.stories.tsx deleted file mode 100644 index dba2c44d4c..0000000000 --- a/frontend/components/PremiumFeatureIconWithTooltip/PremiumFeatureIconWithToooltip.stories.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; - -import PremiumFeatureIconWithTooltip from "./PremiumFeatureIconWithTooltip"; - -const meta: Meta = { - title: "Components/PremiumFeatureIconWithTooltip", - component: PremiumFeatureIconWithTooltip, -}; - -export default meta; - -type Story = StoryObj; - -export const Basic: Story = {}; diff --git a/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss b/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss deleted file mode 100644 index ec9e7a06d9..0000000000 --- a/frontend/components/PremiumFeatureIconWithTooltip/_styles.scss +++ /dev/null @@ -1,5 +0,0 @@ -.premium-icon-tip { - font-style: normal; - font-size: $x-small; - font-weight: normal; -} diff --git a/frontend/components/PremiumFeatureIconWithTooltip/index.ts b/frontend/components/PremiumFeatureIconWithTooltip/index.ts deleted file mode 100644 index f2c2609222..0000000000 --- a/frontend/components/PremiumFeatureIconWithTooltip/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PremiumFeatureIconWithTooltip"; diff --git a/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx b/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx index 27ca436b54..23fd8ce44b 100644 --- a/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx +++ b/frontend/components/TableContainer/DataTable/ActionButton/ActionButton.tsx @@ -1,6 +1,5 @@ import React, { useCallback } from "react"; import { kebabCase, noop } from "lodash"; -import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip"; import { ButtonVariant } from "components/buttons/Button/Button"; import Icon from "components/Icon/Icon"; @@ -17,7 +16,6 @@ export interface IActionButtonProps { hideButton?: boolean | ((targetIds: number[]) => boolean); iconSvg?: IconNames; iconPosition?: string; - indicatePremiumFeature?: boolean; } function useActionCallback( @@ -41,7 +39,6 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => { hideButton, iconSvg, iconPosition, - indicatePremiumFeature, } = buttonProps; const onButtonClick = useActionCallback(onActionButtonClick || noop); @@ -62,14 +59,7 @@ const ActionButton = (buttonProps: IActionButtonProps): JSX.Element | null => { return (
- {indicatePremiumFeature && ( - - )} -
diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx index 9b4f3ddf21..0d351168a2 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx @@ -9,7 +9,6 @@ import PATHS from "router/paths"; import { buildQueryStringFromParams } from "utilities/url"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; import softwareAPI from "services/entities/software"; -import teamPoliciesAPI from "services/entities/team_policies"; import labelsAPI, { getCustomLabels } from "services/entities/labels"; import { QueryContext } from "context/query"; import { AppContext } from "context/app"; @@ -218,11 +217,7 @@ const FleetMaintainedAppDetailsPage = ({ {fleetApp?.name} successfully added. ); - // } } catch (error) { - // quick exit if there was an error adding the software. Skip the policy - // creation. - const ae = (typeof error === "object" ? error : {}) as AxiosResponse; const errorMessage = getErrorMessage(ae); @@ -237,46 +232,8 @@ const FleetMaintainedAppDetailsPage = ({ } else { renderFlash("error", DEFAULT_ERROR_MESSAGE); } - - setShowAddFleetAppSoftwareModal(false); - return; } - // If the install type is automatic we now need to create the new policy. - // if (installType === "automatic" && fleetApp) { - // try { - // await teamPoliciesAPI.create({ - // name: getFleetAppPolicyName(fleetApp.name), - // description: getFleetAppPolicyDescription(fleetApp.name), - // query: getFleetAppPolicyQuery(fleetApp.name), - // team_id: parseInt(teamId, 10), - // software_title_id: titleId, - // platform: "darwin", - // }); - - // renderFlash( - // "success", - // <> - // {fleetApp?.name} successfully added. - // , - // { persistOnPageChange: true } - // ); - // } catch (e) { - // renderFlash("error", AUTOMATIC_POLICY_ERROR_MESSAGE, { - // persistOnPageChange: true, - // }); - // } - - // // for automatic install we redirect on both a successful and error policy - // // add because the software was already successfuly added. - // router.push( - // `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ - // team_id: teamId, - // available_for_install: true, - // })}` - // ); - // } - setShowAddFleetAppSoftwareModal(false); }; diff --git a/frontend/pages/SoftwarePage/helpers.tsx b/frontend/pages/SoftwarePage/helpers.tsx index 044b041849..5ff3b39b3b 100644 --- a/frontend/pages/SoftwarePage/helpers.tsx +++ b/frontend/pages/SoftwarePage/helpers.tsx @@ -64,97 +64,6 @@ export const generateSecretErrMsg = (err: unknown) => { /** Corresponds to automatic_install_policies */ export type InstallType = "manual" | "automatic"; -interface IInstallTypeSection { - className: string; - installType: InstallType; - onChangeInstallType: (value: string) => void; - isCustomPackage?: boolean; - isExeCustomPackage?: boolean; -} - -// Used in FleetAppDetailsForm and PackageForm -export const InstallTypeSection = ({ - className, - installType, - onChangeInstallType, - isCustomPackage = false, - isExeCustomPackage = false, -}: IInstallTypeSection) => { - const isAutomaticDisabled = isExeCustomPackage; - const AUTOMATIC_DISABLED_TOOLTIP = ( - <> - Fleet can't create a policy to detect existing installations for -
.exe packages. To automatically install an .exe, add a custom -
policy and enable the install software automation on the -
Policies page. - - ); - - return ( -
- Install -
- - Manually install on the Host details page for each host. - - } - /> - - Automatically install on each host that's{" "} - - If the host already has any version of this -
software, it won't be installed. - - } - > - missing this software -
- . Policy that triggers install can be customized after software is - added. - - } - /> -
- {installType === "automatic" && isCustomPackage && ( - - } - > - Installing software over existing installations might cause issues. - Fleet's policy may not detect these existing installations. - Please create a test team in Fleet to verify a smooth installation. - - )} -
- ); -}; - export const getInstallType = ( softwarePackage: ISoftwarePackage ): InstallType => { From 8c7a543571b1dda93e5f9b580a14d04e3e7a43c5 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 26 Feb 2025 16:07:59 -0500 Subject: [PATCH 05/15] Fleet UI: Hide more options button when at larger widths (#26632) --- frontend/components/buttons/ActionButtons/_styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/components/buttons/ActionButtons/_styles.scss b/frontend/components/buttons/ActionButtons/_styles.scss index 3c9a41fbda..7842642d26 100644 --- a/frontend/components/buttons/ActionButtons/_styles.scss +++ b/frontend/components/buttons/ActionButtons/_styles.scss @@ -35,5 +35,9 @@ } } } + + @media (min-width: $break-md) { + display: none; + } } } From a0158af6d868ebf2d9fb80a5662860d73121e2c2 Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Wed, 26 Feb 2025 16:20:02 -0600 Subject: [PATCH 06/15] Add SSE endpoint (#26596) For #26218 - Added `GET /api/_version_/fleet/android_enterprise/signup_sse` endpoint and tests - Fixed up handling of Android status reports with a deleted device. We don't actually expect this to happen in production since the proxy should delete the pubSub connection when the enterprise is deleted. # Checklist for submitter - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality --- cmd/fleet/serve.go | 9 ++ server/authz/policy.rego | 2 +- server/mdm/android/pubsub.go | 16 ++- server/mdm/android/service.go | 1 + .../mdm/android/service/enterprises_test.go | 6 + server/mdm/android/service/handler.go | 1 + server/mdm/android/service/pubsub.go | 32 +++-- server/mdm/android/service/service.go | 114 ++++++++++++++++-- .../tests/enterprise/enterprise_test.go | 55 +++++++++ server/mdm/android/tests/testing_utils.go | 28 +++-- 10 files changed, 234 insertions(+), 30 deletions(-) diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index a98ebd05ec..b50e6ba0bd 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -1234,6 +1234,15 @@ the way that the Fleet server works. } req.Body = http.MaxBytesReader(rw, req.Body, fleet.MaxSoftwareInstallerSize) } + + if req.Method == http.MethodGet && strings.HasSuffix(req.URL.Path, "/fleet/android_enterprise/signup_sse") { + // When enabling Android MDM, frontend UI will wait for the admin to finish the setup in Google. + rc := http.NewResponseController(rw) + if err := rc.SetWriteDeadline(time.Now().Add(30 * time.Minute)); err != nil { + level.Error(logger).Log("msg", "http middleware failed to override endpoint write timeout", "err", err) + } + } + apiHandler.ServeHTTP(rw, req) }) diff --git a/server/authz/policy.rego b/server/authz/policy.rego index 937c73d279..55096ded00 100644 --- a/server/authz/policy.rego +++ b/server/authz/policy.rego @@ -1023,7 +1023,7 @@ allow { ## # Android ## -# Global admins can connect enteprise. +# Global admins can connect enterprise. allow { object.type == "android_enterprise" subject.global_role == admin diff --git a/server/mdm/android/pubsub.go b/server/mdm/android/pubsub.go index 998c7fd145..c2c188640d 100644 --- a/server/mdm/android/pubsub.go +++ b/server/mdm/android/pubsub.go @@ -1,10 +1,18 @@ package android +type NotificationType string + const ( - PubSubEnrollment = "ENROLLMENT" - PubSubStatusReport = "STATUS_REPORT" - PubSubCommand = "COMMAND" - PubSubUsageLogs = "USAGE_LOGS" + PubSubEnrollment NotificationType = "ENROLLMENT" + PubSubStatusReport NotificationType = "STATUS_REPORT" + PubSubCommand NotificationType = "COMMAND" + PubSubUsageLogs NotificationType = "USAGE_LOGS" +) + +type DeviceState string + +const ( + DeviceStateDeleted DeviceState = "DELETED" ) type PubSubMessage struct { diff --git a/server/mdm/android/service.go b/server/mdm/android/service.go index 6583c4762a..72a2657e14 100644 --- a/server/mdm/android/service.go +++ b/server/mdm/android/service.go @@ -9,6 +9,7 @@ type Service interface { EnterpriseSignupCallback(ctx context.Context, enterpriseID uint, enterpriseToken string) error GetEnterprise(ctx context.Context) (*Enterprise, error) DeleteEnterprise(ctx context.Context) error + EnterpriseSignupSSE(ctx context.Context) (chan string, error) // CreateEnrollmentToken creates an enrollment token for a new Android device. CreateEnrollmentToken(ctx context.Context, enrollSecret string) (*EnrollmentToken, error) diff --git a/server/mdm/android/service/enterprises_test.go b/server/mdm/android/service/enterprises_test.go index 9a99ed730c..24a8c830d9 100644 --- a/server/mdm/android/service/enterprises_test.go +++ b/server/mdm/android/service/enterprises_test.go @@ -97,6 +97,12 @@ func TestEnterprisesAuth(t *testing.T) { _, err = svc.EnterpriseSignup(ctx) checkAuthErr(t, tt.shouldFailWrite, err) + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + _, err = svc.EnterpriseSignupSSE(ctx) + checkAuthErr(t, tt.shouldFailRead, err) + }) } diff --git a/server/mdm/android/service/handler.go b/server/mdm/android/service/handler.go index a5accf6340..9ede251100 100644 --- a/server/mdm/android/service/handler.go +++ b/server/mdm/android/service/handler.go @@ -25,6 +25,7 @@ func attachFleetAPIRoutes(r *mux.Router, fleetSvc fleet.Service, svc android.Ser ue.GET("/api/_version_/fleet/android_enterprise/signup_url", enterpriseSignupEndpoint, nil) ue.GET("/api/_version_/fleet/android_enterprise", getEnterpriseEndpoint, nil) ue.DELETE("/api/_version_/fleet/android_enterprise", deleteEnterpriseEndpoint, nil) + ue.GET("/api/_version_/fleet/android_enterprise/signup_sse", enterpriseSSE, nil) // ////////////////////////////////////////// // Unauthenticated endpoints diff --git a/server/mdm/android/service/pubsub.go b/server/mdm/android/service/pubsub.go index 7e710d2031..0a5a497bec 100644 --- a/server/mdm/android/service/pubsub.go +++ b/server/mdm/android/service/pubsub.go @@ -50,7 +50,7 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message } } - switch notificationType { + switch android.NotificationType(notificationType) { case android.PubSubEnrollment: var device androidmanagement.Device err := json.Unmarshal(rawData, &device) @@ -59,6 +59,7 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message } err = svc.enrollHost(ctx, &device) if err != nil { + level.Debug(svc.logger).Log("msg", "Error enrolling Android host", "data", rawData) return ctxerr.Wrap(ctx, err, "enrolling Android host") } case android.PubSubStatusReport: @@ -67,6 +68,11 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message if err != nil { return ctxerr.Wrap(ctx, err, "unmarshal Android status report message") } + if device.AppliedState == string(android.DeviceStateDeleted) { + level.Debug(svc.logger).Log("msg", "Android device deleted from MDM", "device.name", device.Name, + "device.enterpriseSpecificId", device.HardwareInfo.EnterpriseSpecificId) + return nil + } host, err := svc.getExistingHost(ctx, &device) if err != nil { return ctxerr.Wrap(ctx, err, "getting existing Android host") @@ -77,11 +83,13 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message "device.enterpriseSpecificId", device.HardwareInfo.EnterpriseSpecificId) err = svc.enrollHost(ctx, &device) if err != nil { + level.Debug(svc.logger).Log("msg", "Error re-enrolling Android host", "data", rawData) return ctxerr.Wrap(ctx, err, "re-enrolling deleted Android host") } } err = svc.updateHost(ctx, &device, host) if err != nil { + level.Debug(svc.logger).Log("msg", "Error updating Android host", "data", rawData) return ctxerr.Wrap(ctx, err, "enrolling Android host") } } @@ -138,6 +146,10 @@ func (svc *Service) validateDevice(ctx context.Context, device *androidmanagemen } func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.Device, host *fleet.AndroidHost) error { + err := svc.validateDevice(ctx, device) + if err != nil { + return err + } if device.AppliedPolicyName != "" { policy, err := svc.getPolicyID(ctx, device) if err != nil { @@ -147,7 +159,7 @@ func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.De if err != nil { return ctxerr.Wrap(ctx, err, "parsing Android policy sync time") } - host.Device.AndroidPolicyID = ptr.Uint(policy) + host.Device.AndroidPolicyID = policy host.Device.LastPolicySyncTime = ptr.Time(policySyncTime) } @@ -216,7 +228,7 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De if err != nil { return ctxerr.Wrap(ctx, err, "parsing Android policy sync time") } - host.Device.AndroidPolicyID = ptr.Uint(policy) + host.Device.AndroidPolicyID = policy host.Device.LastPolicySyncTime = ptr.Time(policySyncTime) } host.SetNodeKey(device.HardwareInfo.EnterpriseSpecificId) @@ -252,14 +264,20 @@ func (svc *Service) getDeviceID(ctx context.Context, device *androidmanagement.D return deviceID, nil } -func (svc *Service) getPolicyID(ctx context.Context, device *androidmanagement.Device) (uint, error) { +func (svc *Service) getPolicyID(ctx context.Context, device *androidmanagement.Device) (*uint, error) { nameParts := strings.Split(device.AppliedPolicyName, "/") if len(nameParts) != 4 { - return 0, ctxerr.Errorf(ctx, "invalid Android policy name: %s", device.AppliedPolicyName) + return nil, ctxerr.Errorf(ctx, "invalid Android policy name: %s", device.AppliedPolicyName) + } + if len(nameParts[3]) == 0 { + level.Error(svc.logger).Log("msg", "Empty Android policy ID", "device.name", device.Name, + "device.enterpriseSpecificID", device.HardwareInfo.EnterpriseSpecificId, "device.AppliedPolicyName", + device.AppliedPolicyName) + return nil, nil } result, err := strconv.ParseUint(nameParts[3], 10, 64) if err != nil { - return 0, ctxerr.Wrap(ctx, err, "parsing Android policy ID") + return nil, ctxerr.Wrapf(ctx, err, "parsing Android policy ID from %s", device.AppliedPolicyName) } - return uint(result), nil + return ptr.Uint(uint(result)), nil } diff --git a/server/mdm/android/service/service.go b/server/mdm/android/service/service.go index e786f67c83..da991dc318 100644 --- a/server/mdm/android/service/service.go +++ b/server/mdm/android/service/service.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/fleetdm/fleet/v4/server" "github.com/fleetdm/fleet/v4/server/authz" @@ -13,11 +14,16 @@ import ( "github.com/fleetdm/fleet/v4/server/mdm/android" "github.com/fleetdm/fleet/v4/server/mdm/android/service/proxy" kitlog "github.com/go-kit/log" + "github.com/go-kit/log/level" "google.golang.org/api/androidmanagement/v1" ) // We use numbers for policy names for easier mapping/indexing with Fleet DB. -const defaultAndroidPolicyID = 1 +const ( + defaultAndroidPolicyID = 1 + DefaultSignupSSEInterval = 3 * time.Second + SignupSSESuccess = "Android Enterprise successfully connected" +) type Service struct { logger kitlog.Logger @@ -25,6 +31,9 @@ type Service struct { ds android.Datastore fleetDS fleet.Datastore proxy android.Proxy + + // SignupSSEInterval can be overwritten in tests. + SignupSSEInterval time.Duration } func NewService( @@ -47,11 +56,12 @@ func NewServiceWithProxy( } return &Service{ - logger: logger, - authz: authorizer, - ds: fleetDS.GetAndroidDS(), - fleetDS: fleetDS, - proxy: proxy, + logger: logger, + authz: authorizer, + ds: fleetDS.GetAndroidDS(), + fleetDS: fleetDS, + proxy: proxy, + SignupSSEInterval: DefaultSignupSSEInterval, }, nil } @@ -155,10 +165,10 @@ func (svc *Service) EnterpriseSignupCallback(ctx context.Context, id uint, enter android.ProxyEnterprisesCreateRequest{ Enterprise: androidmanagement.Enterprise{ EnabledNotificationTypes: []string{ - android.PubSubEnrollment, - android.PubSubStatusReport, - android.PubSubCommand, - android.PubSubUsageLogs, + string(android.PubSubEnrollment), + string(android.PubSubStatusReport), + string(android.PubSubCommand), + string(android.PubSubUsageLogs), }, }, EnterpriseToken: enterpriseToken, @@ -358,3 +368,87 @@ func (svc *Service) checkIfAndroidNotConfigured(ctx context.Context) (*fleet.App } return appConfig, nil } + +type enterpriseSSEResponse struct { + android.DefaultResponse + done chan string +} + +func (r enterpriseSSEResponse) HijackRender(_ context.Context, w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("X-Accel-Buffering", "no") + w.Header().Set("Transfer-Encoding", "chunked") + if r.done == nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprint(w, "Error: No SSE data available") + return + } + w.WriteHeader(http.StatusOK) + w.(http.Flusher).Flush() + + for { + select { + case data, ok := <-r.done: + if ok { + _, _ = fmt.Fprint(w, data) + w.(http.Flusher).Flush() + } + return + case <-time.After(5 * time.Second): + // We send a heartbeat to prevent the load balancer from closing the (otherwise idle) connection. + // The leading colon indicates this is a comment, and is ignored. + // https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events + _, _ = fmt.Fprint(w, ":heartbeat\n") + w.(http.Flusher).Flush() + } + } +} + +func enterpriseSSE(ctx context.Context, _ interface{}, svc android.Service) fleet.Errorer { + done, err := svc.EnterpriseSignupSSE(ctx) + if err != nil { + return android.DefaultResponse{Err: err} + } + return enterpriseSSEResponse{done: done} +} + +func (svc *Service) EnterpriseSignupSSE(ctx context.Context) (chan string, error) { + if err := svc.authz.Authorize(ctx, &android.Enterprise{}, fleet.ActionRead); err != nil { + return nil, err + } + + done := make(chan string) + go func() { + if svc.signupSSECheck(ctx, done) { + return + } + for { + select { + case <-ctx.Done(): + level.Debug(svc.logger).Log("msg", "Context cancelled during Android signup SSE") + return + case <-time.After(svc.SignupSSEInterval): + if svc.signupSSECheck(ctx, done) { + return + } + } + } + }() + + return done, nil +} + +func (svc *Service) signupSSECheck(ctx context.Context, done chan string) bool { + appConfig, err := svc.fleetDS.AppConfig(ctx) + if err != nil { + done <- fmt.Sprintf("Error getting app config: %v", err) + return true + } + if appConfig.MDM.AndroidEnabledAndConfigured { + done <- SignupSSESuccess + return true + } + return false +} diff --git a/server/mdm/android/tests/enterprise/enterprise_test.go b/server/mdm/android/tests/enterprise/enterprise_test.go index d8793239a6..0bdf6f17ba 100644 --- a/server/mdm/android/tests/enterprise/enterprise_test.go +++ b/server/mdm/android/tests/enterprise/enterprise_test.go @@ -1,12 +1,18 @@ package enterprise_test import ( + "context" + "io" "net/http" "testing" + "time" + "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mdm/android" + "github.com/fleetdm/fleet/v4/server/mdm/android/service" "github.com/fleetdm/fleet/v4/server/mdm/android/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -22,6 +28,12 @@ type enterpriseTestSuite struct { func (s *enterpriseTestSuite) SetupSuite() { s.WithServer.SetupSuite(s.T(), "androidEnterpriseTestSuite") s.Token = "bozo" + s.Svc.(*service.Service).SignupSSEInterval = 10 * time.Millisecond +} + +func (s *enterpriseTestSuite) SetupTest() { + s.AppConfig.MDM.AndroidEnabledAndConfigured = false + s.CreateCommonDSMocks() } func (s *enterpriseTestSuite) TearDownSuite() { @@ -29,6 +41,8 @@ func (s *enterpriseTestSuite) TearDownSuite() { } func (s *enterpriseTestSuite) TestGetEnterprise() { + s.SetupTest() + // Enterprise doesn't exist. var resp android.GetEnterpriseResponse s.DoJSON("GET", "/api/v1/fleet/android_enterprise", nil, http.StatusNotFound, &resp) @@ -50,3 +64,44 @@ func (s *enterpriseTestSuite) TestGetEnterprise() { s.Do("DELETE", "/api/v1/fleet/android_enterprise", nil, http.StatusOK) s.DoJSON("GET", "/api/v1/fleet/android_enterprise", nil, http.StatusNotFound, &resp) } + +func (s *enterpriseTestSuite) TestEnterpriseSSE() { + s.SetupTest() + + // Test happy path + resp := s.Do("GET", "/api/v1/fleet/android_enterprise/signup_sse", nil, http.StatusOK) + sseDone := make(chan struct{}) + go func() { + data, err := io.ReadAll(resp.Body) + require.NoError(s.T(), err) + assert.Equal(s.T(), service.SignupSSESuccess, string(data)) + close(sseDone) + }() + + time.Sleep(50 * time.Millisecond) + s.AppConfigMu.Lock() + s.AppConfig.MDM.AndroidEnabledAndConfigured = true + s.AppConfigMu.Unlock() + + select { + case <-sseDone: + s.T().Log("SSE done") + case <-time.After(2 * time.Second): + s.T().Fatal("Timed out waiting for SSE") + } + + // Test with Android already enabled + resp = s.Do("GET", "/api/v1/fleet/android_enterprise/signup_sse", nil, http.StatusOK) + data, err := io.ReadAll(resp.Body) + require.NoError(s.T(), err) + assert.Equal(s.T(), service.SignupSSESuccess, string(data)) + + // Test with error + s.WithServer.FleetDS.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) { + return nil, assert.AnError + } + resp = s.Do("GET", "/api/v1/fleet/android_enterprise/signup_sse", nil, http.StatusOK) + data, err = io.ReadAll(resp.Body) + assert.NoError(s.T(), err) + assert.Contains(s.T(), string(data), assert.AnError.Error()) +} diff --git a/server/mdm/android/tests/testing_utils.go b/server/mdm/android/tests/testing_utils.go index acdf58378f..19c03e9ea0 100644 --- a/server/mdm/android/tests/testing_utils.go +++ b/server/mdm/android/tests/testing_utils.go @@ -5,6 +5,7 @@ import ( "net/http" "net/http/httptest" "os" + "sync" "testing" "github.com/fleetdm/fleet/v4/server/config" @@ -36,18 +37,22 @@ const ( type WithServer struct { suite.Suite - DS *mysql.Datastore - FleetDS ds_mock.Store - Server *httptest.Server - Token string - AppConfig fleet.AppConfig + Svc android.Service + DS *mysql.Datastore + FleetDS ds_mock.Store + Server *httptest.Server + Token string + + AppConfig fleet.AppConfig + AppConfigMu sync.Mutex + Proxy proxy_mock.Proxy ProxyCallbackURL string } func (ts *WithServer) SetupSuite(t *testing.T, dbName string) { ts.DS = CreateNamedMySQLDS(t, dbName) - ts.createCommonDSMocks() + ts.CreateCommonDSMocks() ts.Proxy = proxy_mock.Proxy{} ts.createCommonProxyMocks(t) @@ -56,19 +61,26 @@ func (ts *WithServer) SetupSuite(t *testing.T, dbName string) { logger := kitlog.NewLogfmtLogger(os.Stdout) svc, err := service.NewServiceWithProxy(logger, &ts.FleetDS, &ts.Proxy) require.NoError(t, err) + ts.Svc = svc ts.Server = runServerForTests(t, logger, &fleetSvc, svc) } -func (ts *WithServer) createCommonDSMocks() { +func (ts *WithServer) CreateCommonDSMocks() { ts.FleetDS.GetAndroidDSFunc = func() android.Datastore { return ts.DS } ts.FleetDS.AppConfigFunc = func(_ context.Context) (*fleet.AppConfig, error) { - return &ts.AppConfig, nil + // Create a copy to prevent race conditions + ts.AppConfigMu.Lock() + appConfigCopy := ts.AppConfig + ts.AppConfigMu.Unlock() + return &appConfigCopy, nil } ts.FleetDS.SetAndroidEnabledAndConfiguredFunc = func(_ context.Context, configured bool) error { + ts.AppConfigMu.Lock() ts.AppConfig.MDM.AndroidEnabledAndConfigured = configured + ts.AppConfigMu.Unlock() return nil } } From 1a655bf89adb3322370b247154dfa60ee4207740 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Thu, 27 Feb 2025 14:07:34 +0000 Subject: [PATCH 07/15] UI activites for android mdm (#26647) --- changes/issue-26209-android-mdm-acitivites | 1 + frontend/interfaces/activity.ts | 2 ++ .../GlobalActivityItem/GlobalActivityItem.tsx | 12 ++++++++++++ 3 files changed, 15 insertions(+) create mode 100644 changes/issue-26209-android-mdm-acitivites diff --git a/changes/issue-26209-android-mdm-acitivites b/changes/issue-26209-android-mdm-acitivites new file mode 100644 index 0000000000..7c5da573cc --- /dev/null +++ b/changes/issue-26209-android-mdm-acitivites @@ -0,0 +1 @@ +- add android mdm activities diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index a9dc9bb63d..7324a3ee95 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -103,6 +103,8 @@ export enum ActivityType { DisabledActivityAutomations = "disabled_activity_automations", CanceledScript = "canceled_script", CanceledSoftwareInstall = "canceled_software_install", + EnabledAndroidMdm = "enabled_android_mdm", + DisabledAndroidMdm = "disabled_android_mdm", } /** This is a subset of ActivityType that are shown only for the host past activities */ diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 423ea53824..39696c8899 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -1076,6 +1076,12 @@ const TAGGED_TEMPLATES = { disabledActivityAutomations: () => { return <> disabled activity automations.; }, + enabledAndroidMdm: () => { + return <> turned on Android MDM.; + }, + disabledAndroidMdm: () => { + return <> turned off Android MDM.; + }, }; const getDetail = (activity: IActivity, isPremiumTier: boolean) => { @@ -1314,6 +1320,12 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.DisabledActivityAutomations: { return TAGGED_TEMPLATES.disabledActivityAutomations(); } + case ActivityType.EnabledAndroidMdm: { + return TAGGED_TEMPLATES.enabledAndroidMdm(); + } + case ActivityType.DisabledAndroidMdm: { + return TAGGED_TEMPLATES.disabledAndroidMdm(); + } default: { return TAGGED_TEMPLATES.defaultActivityTemplate(activity); From 7df866754e585852d430c55af797cbeb913cd76e Mon Sep 17 00:00:00 2001 From: Jordan Moore Date: Thu, 27 Feb 2025 09:27:15 -0600 Subject: [PATCH 08/15] Correct a URL in the documentation so it doesn't 404 (#26651) The URL was pointing to a file in the fleet-terraform repo that didn't exist causing it to 404. --- docs/Deploy/deploy-fleet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Deploy/deploy-fleet.md b/docs/Deploy/deploy-fleet.md index bbf8f0e15c..a4e4c9444f 100644 --- a/docs/Deploy/deploy-fleet.md +++ b/docs/Deploy/deploy-fleet.md @@ -81,7 +81,7 @@ This workflow takes about 30 minutes to complete and supports between 10 and 350 ### Instructions -1. [Download](https://github.com/fleetdm/fleet-terraform/tree/mainexample/main.tf) the Fleet `main.tf` Terraform file. +1. [Download](https://github.com/fleetdm/fleet-terraform/blob/main/example/main.tf) the Fleet `main.tf` Terraform file. 2. Edit the following variables in the `main.tf` Terraform file you just downloaded to match your environment: From 75bbbf67316510af503b38e68434e4e56a7f49d5 Mon Sep 17 00:00:00 2001 From: Scott Gress Date: Thu, 27 Feb 2025 09:43:34 -0600 Subject: [PATCH 09/15] Fix issue with policy details modal causing 500 error page (#26628) For #26604 This PR fixes the way we import DOMPurify, so that we can access its `sanitize` method. I'm not sure why this popped up now -- the last release was a month ago. Perhaps a new release of webpack or a related dependency in our build chain? **Before:** ![26604-broken](https://github.com/user-attachments/assets/629567a6-d989-45e2-a90c-eca8f69b1105) --- **After:** ![26604-fixed](https://github.com/user-attachments/assets/4ec580f1-d189-4692-80d2-fee1d3ed8207) --- frontend/components/ClickableUrls/ClickableUrls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/ClickableUrls/ClickableUrls.tsx b/frontend/components/ClickableUrls/ClickableUrls.tsx index 2dffcc2d63..e3f388ec39 100644 --- a/frontend/components/ClickableUrls/ClickableUrls.tsx +++ b/frontend/components/ClickableUrls/ClickableUrls.tsx @@ -1,5 +1,5 @@ import React from "react"; -import * as DOMPurify from "dompurify"; +import DOMPurify from "dompurify"; import classnames from "classnames"; interface IClickableUrls { From 73a368a2005b71af826f8be705a73a23f0a73f0a Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Thu, 27 Feb 2025 10:48:08 -0500 Subject: [PATCH 10/15] Fleet UI: Clean up TabNav and TargetChipSelector components (#26256) --- .../PlatformWrapper/PlatformWrapper.tsx | 9 +- .../PlatformWrapper/_styles.scss | 2 +- .../components/LiveQuery/SelectTargets.tsx | 47 +----- .../TargetChipSelector.stories.tsx | 110 ++++++++++++++ .../TargetChipSelector.tests.tsx | 102 +++++++++++++ .../TargetChipSelector/TargetChipSelector.tsx | 55 +++++++ .../{ => TargetChipSelector}/_styles.scss | 4 +- .../LiveQuery/TargetChipSelector/index.ts | 1 + frontend/components/TabNav/TabNav.stories.tsx | 93 ++++++++++++ frontend/components/TabNav/TabNav.tests.tsx | 68 +++++++++ .../TabsWrapper.tsx => TabNav/TabNav.tsx} | 11 +- frontend/components/TabNav/_styles.scss | 134 ++++++++++++++++++ frontend/components/TabNav/index.ts | 1 + frontend/components/TabText/TabText.tsx | 47 ++++++ frontend/components/TabText/_styles.scss | 23 +++ frontend/components/TabText/index.ts | 1 + frontend/components/TabsWrapper/_styles.scss | 90 ------------ frontend/components/TabsWrapper/index.ts | 1 - frontend/components/icons/Check.tsx | 1 + frontend/components/icons/Plus.tsx | 1 + .../pages/DashboardPage/cards/MDM/MDM.tsx | 15 +- .../DashboardPage/cards/MDM/_styles.scss | 2 +- .../pages/DashboardPage/cards/Munki/Munki.tsx | 15 +- .../DashboardPage/cards/Munki/_styles.scss | 2 +- .../DashboardPage/cards/Software/Software.tsx | 15 +- .../DashboardPage/cards/Software/_styles.scss | 2 +- .../ManageControlsPage/ManageControlsPage.tsx | 9 +- .../components/PlatformTabs/PlatformTabs.tsx | 17 ++- .../SoftwareAddPage/SoftwareAddPage.tsx | 9 +- frontend/pages/SoftwarePage/SoftwarePage.tsx | 9 +- frontend/pages/SoftwarePage/_styles.scss | 2 +- .../components/AddSoftwareModal/_styles.scss | 3 +- frontend/pages/admin/AdminWrapper.tsx | 9 +- .../TeamDetailsWrapper/TeamDetailsWrapper.tsx | 128 ++++++++--------- frontend/pages/admin/_styles.scss | 2 +- .../details/DeviceUserPage/DeviceUserPage.tsx | 28 ++-- .../HostDetailsPage/HostDetailsPage.tsx | 26 ++-- .../details/HostDetailsPage/_styles.scss | 7 - frontend/pages/hosts/details/_styles.scss | 13 +- .../hosts/details/cards/Activity/Activity.tsx | 18 ++- .../hosts/details/cards/Activity/_styles.scss | 15 -- .../SoftwareDetailsModal.tsx | 15 +- .../labels/NewLabelPage/NewLabelPage.tsx | 9 +- .../pages/labels/NewLabelPage/_styles.scss | 2 +- .../PolicyResults/PolicyResults.tsx | 18 +-- .../components/QueryResults/QueryResults.tsx | 16 +-- 46 files changed, 849 insertions(+), 358 deletions(-) create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx create mode 100644 frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx rename frontend/components/LiveQuery/{ => TargetChipSelector}/_styles.scss (92%) create mode 100644 frontend/components/LiveQuery/TargetChipSelector/index.ts create mode 100644 frontend/components/TabNav/TabNav.stories.tsx create mode 100644 frontend/components/TabNav/TabNav.tests.tsx rename frontend/components/{TabsWrapper/TabsWrapper.tsx => TabNav/TabNav.tsx} (65%) create mode 100644 frontend/components/TabNav/_styles.scss create mode 100644 frontend/components/TabNav/index.ts create mode 100644 frontend/components/TabText/TabText.tsx create mode 100644 frontend/components/TabText/_styles.scss create mode 100644 frontend/components/TabText/index.ts delete mode 100644 frontend/components/TabsWrapper/_styles.scss delete mode 100644 frontend/components/TabsWrapper/index.ts diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx index dbacef6bc8..e53ce0e068 100644 --- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx +++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx @@ -13,10 +13,11 @@ import RevealButton from "components/buttons/RevealButton"; // @ts-ignore import InputField from "components/forms/fields/InputField"; import TooltipWrapper from "components/TooltipWrapper"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; import InfoBanner from "components/InfoBanner/InfoBanner"; import CustomLink from "components/CustomLink/CustomLink"; import Radio from "components/forms/fields/Radio"; +import TabText from "components/TabText"; import { isValidPemCertificate } from "../../../pages/hosts/ManageHostsPage/helpers"; import IosIpadosPanel from "./IosIpadosPanel"; @@ -573,7 +574,7 @@ const PlatformWrapper = ({ return (
- + setSelectedTabIndex(index)} selectedIndex={selectedTabIndex} @@ -584,7 +585,7 @@ const PlatformWrapper = ({ // so we add a hidden pseudo element with the same text string return ( - {navItem.name} + {navItem.name} ); })} @@ -601,7 +602,7 @@ const PlatformWrapper = ({ ); })} - +
- ); -}; - const SelectTargets = ({ baseClass, queryId, diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx new file mode 100644 index 0000000000..f5ca496d71 --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.stories.tsx @@ -0,0 +1,110 @@ +import React from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { ISelectLabel, ISelectTeam } from "interfaces/target"; +import TargetChipSelector from "./TargetChipSelector"; // Adjust the path if necessary + +const meta: Meta = { + component: TargetChipSelector, + title: "Components/TargetChipSelector", + argTypes: { + entity: { + description: "The label or team entity to display.", + control: { type: "object" }, + }, + isSelected: { + description: + "Whether the chip is currently selected, updated by parent onClick handler.", + control: { type: "boolean" }, + }, + onClick: { + description: "The handler to call when the chip is clicked.", + action: "clicked", // Use Storybook's action to track clicks + }, + }, + parameters: { + backgrounds: { + default: "light", + values: [ + { name: "light", value: "#ffffff" }, + { name: "dark", value: "#333333" }, + ], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +// Example data for labels and teams +const mockLabel: ISelectLabel = { + id: 1, + name: "Example Label", + label_type: "regular", + description: "A test label", +}; + +const mockTeam: ISelectTeam = { + id: 2, + name: "Example Team", + description: "A test team", +}; + +export const LabelExample: Story = { + args: { + entity: mockLabel, + isSelected: false, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked label:", value); + }, + }, + render: (args) => ( + + ), +}; + +export const TeamExample: Story = { + args: { + entity: mockTeam, + isSelected: true, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked team:", value); + }, + }, + render: (args) => ( + + ), +}; + +export const BuiltInLabelExample: Story = { + args: { + entity: { + id: 3, + name: "MS Windows", + label_type: "builtin", + description: "Microsoft Windows hosts", + }, + isSelected: false, + onClick: (value) => (event) => { + event.preventDefault(); + console.log("Clicked label:", value); + }, + }, + render: (args) => ( + + ), +}; diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx new file mode 100644 index 0000000000..6259fa6490 --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tests.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; + +import { ISelectLabel, ISelectTeam } from "interfaces/target"; +import TargetChipSelector from "./TargetChipSelector"; + +describe("TargetChipSelector", () => { + const mockOnClick = jest.fn(); + + const mockLabel: ISelectLabel = { + id: 1, + name: "Example Label", + label_type: "regular", + description: "A test label", + }; + + const mockTeam: ISelectTeam = { + id: 2, + name: "Example Team", + description: "A test team", + }; + + it("renders the correct display text for a label", () => { + render( + + ); + + expect(screen.getByText("Example Label")).toBeInTheDocument(); + }); + + it("renders the correct display text for a team", () => { + render( + + ); + + expect(screen.getByText("Example Team")).toBeInTheDocument(); + }); + + it("renders the correct icon when selected", () => { + render( + + ); + + expect(screen.getByLabelText("check")).toBeInTheDocument(); + }); + + it("renders the correct icon when not selected", () => { + render( + + ); + + expect(screen.getByLabelText("plus")).toBeInTheDocument(); + }); + + it("calls the onClick handler with the correct entity when clicked", () => { + render( + (event) => mockOnClick(value, event)} + /> + ); + + fireEvent.click(screen.getByRole("button")); + + expect(mockOnClick).toHaveBeenCalledWith(mockLabel, expect.any(Object)); + }); + + it("applies the correct data-selected attribute when selected", () => { + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("data-selected", "true"); + }); + + it("applies the correct data-selected attribute when not selected", () => { + render( + + ); + + const button = screen.getByRole("button"); + expect(button).toHaveAttribute("data-selected", "false"); + }); +}); diff --git a/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx new file mode 100644 index 0000000000..98ad2ccc1e --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/TargetChipSelector.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { + ISelectLabel, + ISelectTeam, + ISelectTargetsEntity, +} from "interfaces/target"; +import Icon from "components/Icon"; +import { + PlatformLabelNameFromAPI, + LABEL_DISPLAY_MAP, +} from "utilities/constants"; + +interface ITargetChipSelectorProps { + entity: ISelectLabel | ISelectTeam; + isSelected: boolean; + onClick: ( + value: ISelectLabel | ISelectTeam + ) => React.MouseEventHandler; +} + +const isBuiltInLabel = ( + entity: ISelectTargetsEntity +): entity is ISelectLabel & { label_type: "builtin" } => { + return "label_type" in entity && entity.label_type === "builtin"; +}; + +const TargetChipSelector = ({ + entity, + isSelected, + onClick, +}: ITargetChipSelectorProps): JSX.Element => { + const displayText = (): string => { + if (isBuiltInLabel(entity)) { + const labelName = entity.name as PlatformLabelNameFromAPI; + if (labelName in LABEL_DISPLAY_MAP) { + return LABEL_DISPLAY_MAP[labelName] || labelName; + } + } + + return entity.name || "Missing display name"; + }; + + return ( + + ); +}; + +export default TargetChipSelector; diff --git a/frontend/components/LiveQuery/_styles.scss b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss similarity index 92% rename from frontend/components/LiveQuery/_styles.scss rename to frontend/components/LiveQuery/TargetChipSelector/_styles.scss index e6e9ce0554..ca844e1354 100644 --- a/frontend/components/LiveQuery/_styles.scss +++ b/frontend/components/LiveQuery/TargetChipSelector/_styles.scss @@ -1,4 +1,4 @@ -.target-pill-selector { +.target-chip-selector { padding: $pad-small; background-color: $core-white; border: none; @@ -34,7 +34,7 @@ } &:hover { - box-shadow: inset 0 0 0 1px $core-vibrant-blue-over; + background-color: $ui-vibrant-blue-10; } &:active { diff --git a/frontend/components/LiveQuery/TargetChipSelector/index.ts b/frontend/components/LiveQuery/TargetChipSelector/index.ts new file mode 100644 index 0000000000..94cc8dca7f --- /dev/null +++ b/frontend/components/LiveQuery/TargetChipSelector/index.ts @@ -0,0 +1 @@ +export { default } from "./TargetChipSelector"; diff --git a/frontend/components/TabNav/TabNav.stories.tsx b/frontend/components/TabNav/TabNav.stories.tsx new file mode 100644 index 0000000000..c86fc9ed64 --- /dev/null +++ b/frontend/components/TabNav/TabNav.stories.tsx @@ -0,0 +1,93 @@ +import React, { useState } from "react"; +import { Meta, StoryObj } from "@storybook/react"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import TabText from "components/TabText"; +import TabNav from "./TabNav"; + +const meta: Meta = { + component: TabNav, + title: "Components/TabNav", + parameters: { + backgrounds: { + default: "light", + values: [ + { + name: "light", + value: "#ffffff", + }, + { + name: "dark", + value: "#333333", + }, + ], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const platformSubNav = [ + { name: Basic tab, type: "type1" }, + { name: Basic tab 2, type: "type2" }, + { + name: Disabled tab, + type: "type3", + disabled: true, + }, + { name: Tab with count, type: "type4" }, + { + name: ( + + Tab with error count + + ), + type: "type5", + }, + ]; + + const renderPanel = (type: string) => { + switch (type) { + case "type1": + return
Content for Tab 1
; + case "type2": + return
Content for Tab 2
; + case "type3": + return
Content for Tab 3
; + case "type4": + return
Content for Tab 4
; + case "type5": + return
Content for Tab 5
; + default: + return null; + } + }; + + return ( + + setSelectedTabIndex(index)} + selectedIndex={selectedTabIndex} + > + + {platformSubNav.map((navItem) => ( + + {navItem.name} + + ))} + + {platformSubNav.map((navItem) => ( + +
{renderPanel(navItem.type)}
+
+ ))} +
+
+ ); + }, +}; diff --git a/frontend/components/TabNav/TabNav.tests.tsx b/frontend/components/TabNav/TabNav.tests.tsx new file mode 100644 index 0000000000..efff13cd9d --- /dev/null +++ b/frontend/components/TabNav/TabNav.tests.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import { render, screen, fireEvent } from "@testing-library/react"; +import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; +import TabText from "components/TabText"; +import TabNav from "./TabNav"; + +describe("TabNav", () => { + it("renders tabs and panels correctly", () => { + render( + + + + + Tab 1 + + + Tab 2 + + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+
+
+ ); + + // Check if tabs are rendered + expect(screen.getByText("Tab 1")).toBeInTheDocument(); + expect(screen.getByText("Tab 2")).toBeInTheDocument(); + + // Check if the first panel content is rendered by default + expect(screen.getByText("Content for Tab 1")).toBeInTheDocument(); + expect(screen.queryByText("Content for Tab 2")).not.toBeInTheDocument(); + }); + + it("switches tabs and displays the correct panel content", () => { + render( + + + + + Tab 1 + + + Tab 2 + + + +
Content for Tab 1
+
+ +
Content for Tab 2
+
+
+
+ ); + + // Switch to the second tab + fireEvent.click(screen.getByText("Tab 2")); + + // Check if the second panel content is displayed + expect(screen.getByText("Content for Tab 2")).toBeInTheDocument(); + expect(screen.queryByText("Content for Tab 1")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/components/TabsWrapper/TabsWrapper.tsx b/frontend/components/TabNav/TabNav.tsx similarity index 65% rename from frontend/components/TabsWrapper/TabsWrapper.tsx rename to frontend/components/TabNav/TabNav.tsx index d788fb4d4e..6b9f59b049 100644 --- a/frontend/components/TabsWrapper/TabsWrapper.tsx +++ b/frontend/components/TabNav/TabNav.tsx @@ -1,7 +1,7 @@ import React from "react"; import classnames from "classnames"; -interface ITabsWrapperProps { +interface ITabNavProps { children: React.ReactChild | React.ReactChild[]; className?: string; } @@ -10,15 +10,12 @@ interface ITabsWrapperProps { * This component exists so we can unify the styles * and overwrite the loaded React Tabs styles. */ -const baseClass = "component__tabs-wrapper"; +const baseClass = "tab-nav"; -const TabsWrapper = ({ - children, - className, -}: ITabsWrapperProps): JSX.Element => { +const TabNav = ({ children, className }: ITabNavProps): JSX.Element => { const classNames = classnames(baseClass, className); return
{children}
; }; -export default TabsWrapper; +export default TabNav; diff --git a/frontend/components/TabNav/_styles.scss b/frontend/components/TabNav/_styles.scss new file mode 100644 index 0000000000..cb5d2a79b9 --- /dev/null +++ b/frontend/components/TabNav/_styles.scss @@ -0,0 +1,134 @@ +.tab-nav { + position: sticky; + top: 0; + background-color: $core-white; + z-index: 2; + + .react-tabs { + &__tab-list { + display: inline-flex; + align-items: flex-start; + gap: $pad-xxlarge; + border-bottom: 1px solid $ui-fleet-black-10; + width: 100%; + height: 43px; + } + .tab-text { + display: flex; /* Ensure text and count are aligned horizontally */ + align-items: center; /* Vertically align items */ + + .tab-text__text { + display: relative; + + // Reserve space for bold text using a hidden pseudo-element + &::before { + content: attr(data-text); /* Same text as the visible one */ + font-weight: bold; /* Mimic bold styling */ + visibility: hidden; /* Keep it invisible */ + position: absolute; /* Prevent it from affecting layout */ + } + } + } + + &__tab { + padding: 5px 0 $pad-medium; + font-size: $x-small; + border: none; + display: inline-flex; + flex-direction: column; + align-items: center; + line-height: 21px; + + &:focus { + box-shadow: none; + outline: 0; + &:after { + left: 0; + bottom: 0; + } + } + + // focus-visible only highlights when tabbing not clicking + &:focus-visible { + .tab-text { + border-radius: $border-radius; + // Outline used instead of border not to shift component + outline: 1px solid $ui-vibrant-blue-25; + outline-offset: -1px; + } + } + + // // Bolding text when the button is active causes a layout shift + // // so we add a hidden pseudo element with the same text string + &:before { + content: attr(data-text); + height: 0; + visibility: hidden; + overflow: hidden; + user-select: none; + pointer-events: none; + font-weight: $bold; + } + + &--selected { + font-weight: $bold; + + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 2px solid $core-vibrant-blue; + position: absolute; + bottom: 0; + left: 0; + } + } + + &:hover { + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 2px solid $core-vibrant-blue; + position: absolute; + bottom: 0; + left: 0; + } + } + + &--disabled { + cursor: not-allowed; + + &:hover { + &::after { + content: ""; + width: 100%; + height: 0; + border-bottom: 0; + position: absolute; + bottom: 0; + left: 0; + } + } + } + + &.no-count:not(.errors-empty).react-tabs__tab--selected::after { + bottom: -2px; + } + } + &__tab-panel { + .no-results-message { + margin-top: $pad-xxlarge; + font-size: $small; + font-weight: $bold; + + span { + margin-top: $pad-medium; + font-size: $x-small; + font-weight: $regular; + display: block; + } + } + } + } +} diff --git a/frontend/components/TabNav/index.ts b/frontend/components/TabNav/index.ts new file mode 100644 index 0000000000..bbb0fa5b23 --- /dev/null +++ b/frontend/components/TabNav/index.ts @@ -0,0 +1 @@ +export { default } from "./TabNav"; diff --git a/frontend/components/TabText/TabText.tsx b/frontend/components/TabText/TabText.tsx new file mode 100644 index 0000000000..10ac42b2d5 --- /dev/null +++ b/frontend/components/TabText/TabText.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import classnames from "classnames"; + +interface ITabTextProps { + className?: string; + children: React.ReactNode; + count?: number; + /** Changes count badge from default purple to red */ + isErrorCount?: boolean; +} + +/* + * This component exists so we can unify the styles + * and add styles to react-tab text. + */ +const baseClass = "tab-text"; + +const TabText = ({ + className, + children, + count, + isErrorCount = false, +}: ITabTextProps): JSX.Element => { + const classNames = classnames(baseClass, className); + + const countClassNames = classnames(`${baseClass}__count`, { + [`${baseClass}__count--error`]: isErrorCount, + }); + + const renderCount = () => { + if (count && count > 0) { + return
{count.toLocaleString()}
; + } + return undefined; + }; + + return ( +
+
+ {children} +
+ {renderCount()} +
+ ); +}; + +export default TabText; diff --git a/frontend/components/TabText/_styles.scss b/frontend/components/TabText/_styles.scss new file mode 100644 index 0000000000..d28705f3ce --- /dev/null +++ b/frontend/components/TabText/_styles.scss @@ -0,0 +1,23 @@ +.tab-text { + display: flex; + flex-direction: row; + gap: $pad-small; + align-items: center; + height: 21px; + + &__count { + display: flex; + padding: 1px 12px; + justify-content: center; + align-items: center; + background-color: $core-vibrant-blue; + border-radius: 29px; + color: $core-white; + font-weight: $bold; + font-size: $xx-small; + + &--error { + background-color: $core-vibrant-red; + } + } +} diff --git a/frontend/components/TabText/index.ts b/frontend/components/TabText/index.ts new file mode 100644 index 0000000000..2f260c4d2c --- /dev/null +++ b/frontend/components/TabText/index.ts @@ -0,0 +1 @@ +export { default } from "./TabText"; diff --git a/frontend/components/TabsWrapper/_styles.scss b/frontend/components/TabsWrapper/_styles.scss deleted file mode 100644 index d4e8d08caa..0000000000 --- a/frontend/components/TabsWrapper/_styles.scss +++ /dev/null @@ -1,90 +0,0 @@ -.component__tabs-wrapper { - position: sticky; - top: 0; - background-color: $core-white; - z-index: 2; - - .react-tabs { - &__tab-list { - border-bottom: 1px solid $ui-gray; - } - &__tab { - padding: $pad-small 0; - margin-right: $pad-xxlarge; - font-size: $x-small; - border: none; - display: inline-flex; - flex-direction: column; - align-items: center; - line-height: 19px; // Fix shifty bold text - - &:focus { - box-shadow: none; - outline: 0; - &:after { - left: 0; - bottom: 0; - } - } - - // focus-visible only highlights when tabbing not clicking - &:focus-visible { - background-color: $ui-vibrant-blue-10; - } - - // Bolding text when the button is active causes a layout shift - // so we add a hidden pseudo element with the same text string - &:before { - content: attr(data-text); - height: 0; - visibility: hidden; - overflow: hidden; - user-select: none; - pointer-events: none; - font-weight: $bold; - } - &--selected { - font-weight: $bold; - - &::after { - content: ""; - width: 100%; - height: 0; - border-bottom: 2px solid #6a67fe; - position: absolute; - bottom: 0; - left: 0; - } - } - &--disabled { - cursor: not-allowed; - } - &.no-count:not(.errors-empty).react-tabs__tab--selected::after { - bottom: -2px; - } - .count { - margin-right: $pad-small; - padding: $pad-xxsmall 12px; - background-color: $core-vibrant-red; - display: inline-block; - border-radius: 29px; - color: $core-white; - font-weight: $bold; - } - } - &__tab-panel { - .no-results-message { - margin-top: $pad-xxlarge; - font-size: $small; - font-weight: $bold; - - span { - margin-top: $pad-medium; - font-size: $x-small; - font-weight: $regular; - display: block; - } - } - } - } -} diff --git a/frontend/components/TabsWrapper/index.ts b/frontend/components/TabsWrapper/index.ts deleted file mode 100644 index 980c53aaa0..0000000000 --- a/frontend/components/TabsWrapper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TabsWrapper"; diff --git a/frontend/components/icons/Check.tsx b/frontend/components/icons/Check.tsx index f84a2ffd1f..106f6e00c3 100644 --- a/frontend/components/icons/Check.tsx +++ b/frontend/components/icons/Check.tsx @@ -13,6 +13,7 @@ const Check = ({ color = "core-fleet-blue" }: ICheckProps) => { height="16" fill="none" viewBox="0 0 16 16" + aria-label="check" > { fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" + aria-label="plus" > )}
- + - Solutions - Status + + Solutions + + + Status + {error ? ( @@ -189,7 +194,7 @@ const Mdm = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/MDM/_styles.scss b/frontend/pages/DashboardPage/cards/MDM/_styles.scss index 29ba4c319c..e78a190404 100644 --- a/frontend/pages/DashboardPage/cards/MDM/_styles.scss +++ b/frontend/pages/DashboardPage/cards/MDM/_styles.scss @@ -3,7 +3,7 @@ position: relative; height: 100%; // centers loading spinner - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } diff --git a/frontend/pages/DashboardPage/cards/Munki/Munki.tsx b/frontend/pages/DashboardPage/cards/Munki/Munki.tsx index 8b12f2d2a1..7ac7cf6a8a 100644 --- a/frontend/pages/DashboardPage/cards/Munki/Munki.tsx +++ b/frontend/pages/DashboardPage/cards/Munki/Munki.tsx @@ -6,7 +6,8 @@ import { IMunkiVersionsAggregate, } from "interfaces/macadmins"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import TableContainer from "components/TableContainer"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; @@ -58,11 +59,15 @@ const Munki = ({
)}
- + - Issues - Versions + + Issues + + + Versions + {errorMacAdmins ? ( @@ -128,7 +133,7 @@ const Munki = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/Munki/_styles.scss b/frontend/pages/DashboardPage/cards/Munki/_styles.scss index a447f7aa36..39ba88050b 100644 --- a/frontend/pages/DashboardPage/cards/Munki/_styles.scss +++ b/frontend/pages/DashboardPage/cards/Munki/_styles.scss @@ -5,7 +5,7 @@ .data-table__wrapper { overflow-x: auto; } - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } .data-table-block { diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index a8b8d98741..3aafa34a73 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -8,7 +8,8 @@ import { buildQueryStringFromParams } from "utilities/url"; import { ISoftwareResponse } from "interfaces/software"; import { ITableQueryData } from "components/TableContainer/TableContainer"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; @@ -76,11 +77,15 @@ const Software = ({ )}
- + - All - Vulnerable + + All + + + Vulnerable + {!isSoftwareFetching && errorSoftware ? ( @@ -129,7 +134,7 @@ const Software = ({ )} - +
); diff --git a/frontend/pages/DashboardPage/cards/Software/_styles.scss b/frontend/pages/DashboardPage/cards/Software/_styles.scss index 3535b1f320..71ea46b649 100644 --- a/frontend/pages/DashboardPage/cards/Software/_styles.scss +++ b/frontend/pages/DashboardPage/cards/Software/_styles.scss @@ -19,7 +19,7 @@ .form-field--dropdown { margin: 0; } - .component__tabs-wrapper .table-container__header { + .tab-nav .table-container__header { display: none; } &__empty-software { diff --git a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx index c278cd5a3e..71570f209e 100644 --- a/frontend/pages/ManageControlsPage/ManageControlsPage.tsx +++ b/frontend/pages/ManageControlsPage/ManageControlsPage.tsx @@ -6,7 +6,8 @@ import PATHS from "router/paths"; import { AppContext } from "context/app"; import useTeamIdParam from "hooks/useTeamIdParam"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import MainContent from "components/MainContent"; import TeamsDropdown from "components/TeamsDropdown"; import { parseOSUpdatesCurrentVersionsQueryParams } from "./OSUpdates/components/CurrentVersionSection/CurrentVersionSection"; @@ -112,7 +113,7 @@ const ManageControlsPage = ({ const renderBody = () => { return (
- + { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { teamIdForApi, currentPage: page, diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx index d69a9ec838..bc02c5800f 100644 --- a/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx +++ b/frontend/pages/ManageControlsPage/OSUpdates/components/PlatformTabs/PlatformTabs.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Tab, TabList, TabPanel, Tabs } from "react-tabs"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import CustomLink from "components/CustomLink"; import { SUPPORT_LINK } from "utilities/constants"; @@ -62,27 +63,25 @@ const PlatformTabs = ({ return (
- + - {/* Bolding text when the tab is active causes a layout shift so - we add a hidden pseudo element with the same text string */} - macOS + macOS {isWindowsMdmEnabled && ( - Windows + Windows )} - iOS + iOS - iPadOS + iPadOS {isAndroidMdmEnabled && ( @@ -149,7 +148,7 @@ const PlatformTabs = ({ )} - +
); }; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx index ef9f244ab2..fa326a9a5d 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx @@ -11,7 +11,8 @@ import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; import MainContent from "components/MainContent"; import BackLink from "components/BackLink"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import SidePanelContent from "components/SidePanelContent"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; @@ -112,7 +113,7 @@ const SoftwareAddPage = ({ className={`${baseClass}__back-to-software`} />

Add software

- + { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { router, currentTeamId: parseInt(location.query.team_id, 10), diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx index 21a8e18d55..0beceae5c3 100644 --- a/frontend/pages/SoftwarePage/SoftwarePage.tsx +++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx @@ -31,7 +31,8 @@ import { getNextLocationPath } from "utilities/helpers"; import Button from "components/buttons/Button"; import MainContent from "components/MainContent"; import TeamsHeader from "components/TeamsHeader"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import ManageAutomationsModal from "./components/ManageSoftwareAutomationsModal"; import AddSoftwareModal from "./components/AddSoftwareModal"; @@ -412,7 +413,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { const renderBody = () => { return (
- + { {softwareSubNav.map((navItem) => { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { router, isSoftwareEnabled: Boolean( diff --git a/frontend/pages/SoftwarePage/_styles.scss b/frontend/pages/SoftwarePage/_styles.scss index fdbe0bea5d..5500c122f1 100644 --- a/frontend/pages/SoftwarePage/_styles.scss +++ b/frontend/pages/SoftwarePage/_styles.scss @@ -57,7 +57,7 @@ } &__wrapper { - .component__tabs-wrapper { + .tab-nav { margin-bottom: $pad-xxlarge; } } diff --git a/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss b/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss index 706272708b..cd58cf7729 100644 --- a/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss +++ b/frontend/pages/SoftwarePage/components/AddSoftwareModal/_styles.scss @@ -1,7 +1,6 @@ .add-software-modal { - // have to use this selector to override the default styles - .component__tabs-wrapper { + .tab-nav { margin-bottom: 0; } } diff --git a/frontend/pages/admin/AdminWrapper.tsx b/frontend/pages/admin/AdminWrapper.tsx index 9efa69ae67..98666ac002 100644 --- a/frontend/pages/admin/AdminWrapper.tsx +++ b/frontend/pages/admin/AdminWrapper.tsx @@ -4,8 +4,9 @@ import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; import { AppContext } from "context/app"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; import MainContent from "components/MainContent"; +import TabText from "components/TabText"; import classnames from "classnames"; interface ISettingSubNavItem { @@ -77,7 +78,7 @@ const AdminWrapper = ({ return (
- +

Settings

-
+ {children}
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx index 3a35856f60..eaf07701c0 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx @@ -24,7 +24,8 @@ import sortUtils from "utilities/sort"; import ActionButtons from "components/buttons/ActionButtons/ActionButtons"; import Spinner from "components/Spinner"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import BackLink from "components/BackLink"; import TeamsDropdown from "components/TeamsDropdown"; import MainContent from "components/MainContent"; @@ -387,69 +388,66 @@ const TeamDetailsWrapper = ({ return ( <> - - {isGlobalAdmin ? ( -
- -
- ) : ( - <> - )} -
-
- {userTeams?.length === 1 ? ( -

{currentTeamDetails.name}

- ) : ( - - )} - {!!hostsTotalDisplay && ( - - {hostsTotalDisplay} - - )} -
- + {isGlobalAdmin ? ( +
+
+ ) : ( + <> + )} +
+
+ {userTeams?.length === 1 ? ( +

{currentTeamDetails.name}

+ ) : ( + + )} + {!!hostsTotalDisplay && ( + + {hostsTotalDisplay} + + )} +
+ +
+ - + {showAddHostsModal && ( - + router.push(tabPaths[i])} > - Details + + Details + {isPremiumTier && isSoftwareEnabled && hasSelfService && ( - Self-service + + Self-service + + )} + {isSoftwareEnabled && ( + + Software + )} - {isSoftwareEnabled && Software} {isPremiumTier && ( -
- {failingPoliciesCount > 0 && ( - {failingPoliciesCount} - )} + Policies -
+
)}
@@ -461,7 +467,7 @@ const DeviceUserPage = ({ )}
-
+ {showInfoModal && } {showEnrollMdmModal && (host.dep_assigned_to_fleet ? ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 8f72c023ec..dd5d715c00 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -45,7 +45,8 @@ import { import { isAndroid, isIPadOrIPhone } from "interfaces/platform"; import Spinner from "components/Spinner"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import MainContent from "components/MainContent"; import BackLink from "components/BackLink"; import RunScriptDetailsModal from "pages/DashboardPage/cards/ActivityFeed/components/RunScriptDetailsModal"; @@ -124,6 +125,7 @@ interface IHostDetailsSubNavItem { name: string | JSX.Element; title: string; pathname: string; + count?: number; } const DEFAULT_ACTIVITY_PAGE_SIZE = 8; @@ -755,16 +757,10 @@ const HostDetailsPage = ({ pathname: PATHS.HOST_QUERIES(hostIdFromURL), }, { - name: ( - <> - {failingPoliciesCount > 0 && ( - {failingPoliciesCount} - )} - Policies - - ), + name: "Policies", title: "policies", pathname: PATHS.HOST_POLICIES(hostIdFromURL), + count: failingPoliciesCount, }, ]; @@ -844,7 +840,7 @@ const HostDetailsPage = ({ )} hostMdmDeviceStatus={hostMdmDeviceStatus} /> - + navigateToNav(i)} @@ -853,7 +849,13 @@ const HostDetailsPage = ({ {hostDetailsSubNav.map((navItem) => { // Bolding text when the tab is active causes a layout shift // so we add a hidden pseudo element with the same text string - return {navItem.name}; + return ( + + + {navItem.name} + + + ); })} @@ -956,7 +958,7 @@ const HostDetailsPage = ({ /> - + {showDeleteHostModal && ( setShowDeleteHostModal(false)} diff --git a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss index 25f6e705f6..00621284c3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss @@ -1,11 +1,4 @@ .host-details { - &__tabs-wrapper { - .react-tabs__tab { - display: inline-flex; - flex-direction: row; - } - } - // grid layout styles for the host details page &__details-panel { display: grid; diff --git a/frontend/pages/hosts/details/_styles.scss b/frontend/pages/hosts/details/_styles.scss index cd225a7577..d9efc254f5 100644 --- a/frontend/pages/hosts/details/_styles.scss +++ b/frontend/pages/hosts/details/_styles.scss @@ -71,24 +71,15 @@ } } - &__tabs-wrapper { + &__tab-nav { background-color: $ui-off-white; width: 100%; // direct descendant of selector allows us to only change the first level of // tab styling and not change the tabs inside the cards. > .react-tabs > .react-tabs__tab-list { - .react-tabs__tab { - padding: 6px 0px 16px 0px; - margin-right: $pad-xxlarge; - } .react-tabs__tab--selected { background-color: $ui-off-white; - - // When tabbing through the app - &:focus-visible { - background-color: $ui-vibrant-blue-10; - } } } } @@ -160,6 +151,6 @@ // we dont need the margin on the host details page as we are not using grid css // for the spacing. -.host-details__tabs-wrapper .card { +.host-details__tab-nav .card { margin-top: 0; } diff --git a/frontend/pages/hosts/details/cards/Activity/Activity.tsx b/frontend/pages/hosts/details/cards/Activity/Activity.tsx index 58646d7661..97739410a1 100644 --- a/frontend/pages/hosts/details/cards/Activity/Activity.tsx +++ b/frontend/pages/hosts/details/cards/Activity/Activity.tsx @@ -8,7 +8,8 @@ import { } from "services/entities/activities"; import Card from "components/Card"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import Spinner from "components/Spinner"; import TooltipWrapper from "components/TooltipWrapper"; import { ShowActivityDetailsHandler } from "components/ActivityItem/ActivityItem"; @@ -67,20 +68,17 @@ const Activity = ({
)}

Activity

- + - Past - Upcoming - {!!upcomingCount && ( - - {upcomingCount} - - )} + Past + + + Upcoming @@ -106,7 +104,7 @@ const Activity = ({ /> - + ); }; diff --git a/frontend/pages/hosts/details/cards/Activity/_styles.scss b/frontend/pages/hosts/details/cards/Activity/_styles.scss index 7fc58cd091..3aee164762 100644 --- a/frontend/pages/hosts/details/cards/Activity/_styles.scss +++ b/frontend/pages/hosts/details/cards/Activity/_styles.scss @@ -6,21 +6,6 @@ margin: 0 0 $pad-large; } - .react-tabs__tab-list { - li:first-of-type { - padding-bottom: 10px; // adds 2px to bottom padding of "Past" tab to adjust for the count badge on the "Upcoming" tab - } - } - - &__upcoming-count { - padding: $pad-xxsmall $pad-xsmall; - color: $core-white; - background-color: $core-vibrant-blue; - border-radius: $border-radius; - font-weight: $bold; - margin-left: $pad-small; - } - &__loading-overlay { height: 100%; width: 100%; diff --git a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx index 74c30fd026..078d7158b3 100644 --- a/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx +++ b/frontend/pages/hosts/details/cards/Software/SoftwareDetailsModal/SoftwareDetailsModal.tsx @@ -11,7 +11,8 @@ import { } from "interfaces/software"; import Modal from "components/Modal"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import Button from "components/buttons/Button"; import DataSet from "components/DataSet"; import { dateAgo } from "utilities/date_format"; @@ -169,11 +170,15 @@ const TabsContent = ({ software: IHostSoftware; }) => { return ( - + - Software details - Install details + + Software details + + + Install details + @@ -185,7 +190,7 @@ const TabsContent = ({ /> - + ); }; diff --git a/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx b/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx index 1f18639c42..10ff28f75f 100644 --- a/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx +++ b/frontend/pages/labels/NewLabelPage/NewLabelPage.tsx @@ -6,7 +6,8 @@ import useToggleSidePanel from "hooks/useToggleSidePanel"; import MainContent from "components/MainContent"; import SidePanelContent from "components/SidePanelContent"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; import PATHS from "router/paths"; @@ -79,7 +80,7 @@ const NewLabelPage = ({ router, location, children }: INewLabelPageProps) => { Dynamic (smart) labels are assigned to hosts if the query returns results. Manual labels are assigned to selected hosts.

- + { {labelSubNav.map((navItem) => { return ( - {navItem.name} + {navItem.name} ); })} - + {React.cloneElement(children, { showOpenSidebarButton, onOpenSidebar, diff --git a/frontend/pages/labels/NewLabelPage/_styles.scss b/frontend/pages/labels/NewLabelPage/_styles.scss index 637d670a31..bb1592ba83 100644 --- a/frontend/pages/labels/NewLabelPage/_styles.scss +++ b/frontend/pages/labels/NewLabelPage/_styles.scss @@ -9,7 +9,7 @@ font-size: $xx-small; } - &__new-label-tabs-wrapper { + &__new-label-tab-nav { margin-bottom: $pad-xxlarge; } } diff --git a/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx b/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx index b78ffbb171..07f2aa5c3a 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx +++ b/frontend/pages/policies/PolicyPage/components/PolicyResults/PolicyResults.tsx @@ -15,7 +15,8 @@ import { ITarget } from "interfaces/target"; import Button from "components/buttons/Button"; import Icon from "components/Icon/Icon"; -import TabsWrapper from "components/TabsWrapper"; +import TabNav from "components/TabNav"; +import TabText from "components/TabText"; import InfoBanner from "components/InfoBanner"; import ShowQueryModal from "components/modals/ShowQueryModal"; import TooltipWrapper from "components/TooltipWrapper"; @@ -240,23 +241,22 @@ const PolicyResults = ({ onClickRunAgain={onRunQuery} onClickStop={onStopQuery} /> - + setNavTabIndex(i)}> - {NAV_TITLES.RESULTS} + + {NAV_TITLES.RESULTS} + - - {errors?.length > 0 && ( - {errors.length} - )} + {NAV_TITLES.ERRORS} - + {renderResultsTable()} {renderErrorsTable()} - + {showQueryModal && ( )} - + setNavTabIndex(i)}> {NAV_TITLES.RESULTS} - - {errors?.length > 0 && ( - - {errors.length.toLocaleString()} - - )} + {NAV_TITLES.ERRORS} - + {renderResultsTab()} {renderErrorsTab()} - + {showQueryModal && ( Date: Thu, 27 Feb 2025 10:53:34 -0500 Subject: [PATCH 11/15] Fleet UI: Fix several team ids that were dropping in certain flows (#26590) --- changes/26569-team-id-dropping-bug | 1 + .../PlatformSelector/PlatformSelector.tsx | 9 +- .../SandboxExpiryMessage.tsx | 62 -------- .../Sandbox/SandboxExpiryMessage/_styles.scss | 34 ----- .../Sandbox/SandboxExpiryMessage/index.ts | 1 - .../ViewAllHostsLink/ViewAllHostsLink.tsx | 6 +- .../top_nav/SiteTopNav/SiteTopNav.tsx | 6 +- .../LowDiskSpaceHosts/LowDiskSpaceHosts.tsx | 12 +- .../cards/MissingHosts/MissingHosts.tsx | 6 +- .../cards/OperatingSystems/OSTableConfig.tsx | 12 +- .../DashboardPage/cards/Software/Software.tsx | 11 +- .../cards/TotalHosts/TotalHosts.tsx | 10 +- .../PlatformHostCounts/PlatformHostCounts.tsx | 37 ++--- .../ProfileStatusAggregate.tsx | 16 +-- .../DiskEncryptionTable.tsx | 6 +- .../EditScriptModal/EditScriptModal.tsx | 21 ++- .../ScriptDetailsModal/ScriptDetailsModal.tsx | 21 ++- .../SoftwareAddPage/SoftwareAddPage.tsx | 14 +- .../SoftwareAppStoreVpp.tsx | 6 +- .../SoftwareCustomPackage.tsx | 8 +- .../FleetMaintainedAppDetailsPage.tsx | 14 +- .../FleetMaintainedAppsTable.tsx | 11 +- .../FleetMaintainedAppsTableConfig.tsx | 11 +- .../SoftwareOSTable/SoftwareOSTable.tsx | 12 +- frontend/pages/SoftwarePage/SoftwarePage.tsx | 16 ++- .../AutomaticInstallModal.tsx | 12 +- .../SoftwareInstallerCard.tsx | 6 +- .../SoftwareTitleDetailsPage.tsx | 12 +- .../SoftwareTitleDetailsTable.tsx | 12 +- .../SoftwareTitleDetailsTableConfig.tsx | 10 +- .../SoftwareTable/SoftwareTable.tsx | 13 +- .../SoftwareTitlesTableConfig.tsx | 10 +- .../SoftwareVersionsTableConfig.tsx | 14 +- .../SoftwareVulnerabilitiesTable.tsx | 14 +- .../VulnerabilitiesTableConfig.tsx | 10 +- .../SoftwareVulnOSVersions.tsx | 10 +- .../SwVulnOSTableConfig.tsx | 10 +- .../SoftwareVulnSoftwareVersions.tsx | 18 +-- .../SwVulnSwTableConfig.tsx | 10 +- .../SoftwareVulnerabilitiesTable.tsx | 14 +- .../SoftwareVulnerabilitiesTableConfig.tsx | 12 +- .../cards/Calendars/Calendars.tsx | 7 +- .../HostDetailsPage/HostDetailsPage.tsx | 1 + .../ScriptModalGroup/ScriptModalGroup.tsx | 3 + .../SelectQueryModal/SelectQueryModal.tsx | 15 +- .../HostQueryReport/HostQueryReport.tsx | 32 +++-- .../details/cards/Policies/HostPolicies.tsx | 18 +-- .../Software/HostSoftwareTableConfig.tsx | 6 +- .../DynamicLabel/DynamicLabel.tsx | 8 +- .../NewLabelPage/ManualLabel/ManualLabel.tsx | 8 +- .../InstallSoftwareModal.tsx | 12 +- .../PoliciesTable/PoliciesTableConfig.tsx | 134 +++++++++--------- .../PolicyRunScriptModal.tsx | 11 +- .../pages/policies/PolicyPage/PolicyPage.tsx | 6 +- .../PolicyPage/screens/QueryEditor.tsx | 16 ++- .../ManageQueriesPage/ManageQueriesPage.tsx | 9 +- .../components/QueriesTable/QueriesTable.tsx | 15 +- .../QueriesTable/QueriesTableConfig.tsx | 78 +++++----- .../QueryDetailsPage/QueryDetailsPage.tsx | 24 +++- frontend/pages/queries/edit/EditQueryPage.tsx | 66 +++++---- .../EditQueryForm/EditQueryForm.tsx | 28 ++-- .../live/LiveQueryPage/LiveQueryPage.tsx | 23 +-- frontend/router/page_titles.ts | 2 +- frontend/router/paths.ts | 51 ++----- frontend/utilities/helpers.tsx | 25 ---- frontend/utilities/url/index.ts | 19 +++ frontend/utilities/url/url.tests.ts | 124 ++++++++++------ 67 files changed, 657 insertions(+), 634 deletions(-) create mode 100644 changes/26569-team-id-dropping-bug delete mode 100644 frontend/components/Sandbox/SandboxExpiryMessage/SandboxExpiryMessage.tsx delete mode 100644 frontend/components/Sandbox/SandboxExpiryMessage/_styles.scss delete mode 100644 frontend/components/Sandbox/SandboxExpiryMessage/index.ts diff --git a/changes/26569-team-id-dropping-bug b/changes/26569-team-id-dropping-bug new file mode 100644 index 0000000000..378a8ff17b --- /dev/null +++ b/changes/26569-team-id-dropping-bug @@ -0,0 +1 @@ +- Fleet UI: Fixed several links that were dropping team_id parameters resetting team to All teams diff --git a/frontend/components/PlatformSelector/PlatformSelector.tsx b/frontend/components/PlatformSelector/PlatformSelector.tsx index 54bbfbcd82..a9955c6a68 100644 --- a/frontend/components/PlatformSelector/PlatformSelector.tsx +++ b/frontend/components/PlatformSelector/PlatformSelector.tsx @@ -5,7 +5,7 @@ import { IPolicySoftwareToInstall } from "interfaces/policy"; import Checkbox from "components/forms/fields/Checkbox"; import CustomLink from "components/CustomLink"; import TooltipWrapper from "components/TooltipWrapper"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import paths from "router/paths"; interface IPlatformSelectorProps { @@ -49,9 +49,10 @@ export const PlatformSelector = ({ } const softwareName = installSoftware.name; const softwareId = installSoftware.software_title_id.toString(); - const softwareLink = `${paths.SOFTWARE_TITLE_DETAILS( - softwareId - )}?${buildQueryStringFromParams({ team_id: currentTeamId })}`; + const softwareLink = getPathWithQueryParams( + paths.SOFTWARE_TITLE_DETAILS(softwareId), + { team_id: currentTeamId } + ); return ( diff --git a/frontend/components/Sandbox/SandboxExpiryMessage/SandboxExpiryMessage.tsx b/frontend/components/Sandbox/SandboxExpiryMessage/SandboxExpiryMessage.tsx deleted file mode 100644 index a8d3f130b6..0000000000 --- a/frontend/components/Sandbox/SandboxExpiryMessage/SandboxExpiryMessage.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import { browserHistory } from "react-router"; -import PATHS from "router/paths"; - -import Button from "components/buttons/Button"; -import Icon from "components/Icon"; - -const baseClass = "sandbox-expiry-message"; - -interface ISandboxExpiryMessageProps { - expiry: string; - noSandboxHosts?: boolean; -} - -const SandboxExpiryMessage = ({ - expiry, - noSandboxHosts, -}: ISandboxExpiryMessageProps) => { - const openAddHostModal = () => { - browserHistory.push(PATHS.MANAGE_HOSTS_ADD_HOSTS); - }; - - if (noSandboxHosts) { - return ( -
-

Your Fleet Sandbox expires in {expiry}.

-
- -

- Quick tip: Enroll a host to get started. -

-
- -
-
-
- ); - } - - return ( - -

Your Fleet Sandbox expires in {expiry}.

-

- Learn how to use Fleet{" "} - -

-
- ); -}; - -export default SandboxExpiryMessage; diff --git a/frontend/components/Sandbox/SandboxExpiryMessage/_styles.scss b/frontend/components/Sandbox/SandboxExpiryMessage/_styles.scss deleted file mode 100644 index b19dd14b30..0000000000 --- a/frontend/components/Sandbox/SandboxExpiryMessage/_styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -.sandbox-expiry-message { - display: flex; - justify-content: space-between; - align-items: center; - padding: $pad-large $pad-xlarge; - margin-bottom: $pad-large; - background-color: $ui-vibrant-blue-10; - border: 1px solid $ui-vibrant-blue-50; - border-radius: $border-radius; - font-size: $x-small; - color: $core-fleet-black; - font-weight: $regular; - position: relative; // Position in front of settings sticky header space - z-index: 9; // Position in front of settings sticky header space - - p { - margin: 0; - } - - &__tip { - display: flex; - align-items: center; - gap: $pad-small; - - button { - margin-left: $pad-small; - } - } - - .button { - font-size: $xx-small; - font-weight: $bold; - } -} diff --git a/frontend/components/Sandbox/SandboxExpiryMessage/index.ts b/frontend/components/Sandbox/SandboxExpiryMessage/index.ts deleted file mode 100644 index 6269a66378..0000000000 --- a/frontend/components/Sandbox/SandboxExpiryMessage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./SandboxExpiryMessage"; diff --git a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx index 9ef3a78bba..6fe25c7d91 100644 --- a/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx +++ b/frontend/components/ViewAllHostsLink/ViewAllHostsLink.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router"; import classnames from "classnames"; import Icon from "components/Icon"; -import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import { getPathWithQueryParams, QueryParams } from "utilities/url"; interface IHostLinkProps { queryParams?: QueryParams; @@ -44,9 +44,7 @@ const ViewAllHostsLink = ({ ? PATHS.MANAGE_HOSTS_LABEL(platformLabelId) : PATHS.MANAGE_HOSTS; - const path = queryParams - ? `${endpoint}?${buildQueryStringFromParams(queryParams)}` - : endpoint; + const path = getPathWithQueryParams(endpoint, queryParams); return ( { if (currentQueryParams.team_id !== API_ALL_TEAMS_ID) { - return `${path}?team_id=${currentQueryParams.team_id}`; + return getPathWithQueryParams(path, { + team_id: currentQueryParams.team_id, + }); } return activePath; }; diff --git a/frontend/pages/DashboardPage/cards/LowDiskSpaceHosts/LowDiskSpaceHosts.tsx b/frontend/pages/DashboardPage/cards/LowDiskSpaceHosts/LowDiskSpaceHosts.tsx index b921423cdb..faf7bcc4f2 100644 --- a/frontend/pages/DashboardPage/cards/LowDiskSpaceHosts/LowDiskSpaceHosts.tsx +++ b/frontend/pages/DashboardPage/cards/LowDiskSpaceHosts/LowDiskSpaceHosts.tsx @@ -1,7 +1,7 @@ import React from "react"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import HostCountCard from "../HostCountCard"; @@ -24,15 +24,13 @@ const LowDiskSpaceHosts = ({ }: ILowDiskSpaceHostsProps): JSX.Element => { // build the manage hosts URL filtered by low disk space only // currently backend cannot filter by both low disk space and label - const queryParams = { - low_disk_space: lowDiskSpaceGb, - team_id: currentTeamId, - }; - const queryString = buildQueryStringFromParams(queryParams); const endpoint = selectedPlatformLabelId ? PATHS.MANAGE_HOSTS_LABEL(selectedPlatformLabelId) : PATHS.MANAGE_HOSTS; - const path = `${endpoint}?${queryString}`; + const path = getPathWithQueryParams(endpoint, { + low_disk_space: lowDiskSpaceGb, + team_id: currentTeamId, + }); const tooltipText = notSupported ? "Disk space info is not available for Chromebooks." diff --git a/frontend/pages/DashboardPage/cards/MissingHosts/MissingHosts.tsx b/frontend/pages/DashboardPage/cards/MissingHosts/MissingHosts.tsx index 1c50408ddb..8ea0e62e28 100644 --- a/frontend/pages/DashboardPage/cards/MissingHosts/MissingHosts.tsx +++ b/frontend/pages/DashboardPage/cards/MissingHosts/MissingHosts.tsx @@ -1,7 +1,7 @@ import React from "react"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import HostCountCard from "../HostCountCard"; @@ -23,11 +23,11 @@ const MissingHosts = ({ status: "missing", team_id: currentTeamId, }; - const queryString = buildQueryStringFromParams(queryParams); + const endpoint = selectedPlatformLabelId ? PATHS.MANAGE_HOSTS_LABEL(selectedPlatformLabelId) : PATHS.MANAGE_HOSTS; - const path = `${endpoint}?${queryString}`; + const path = getPathWithQueryParams(endpoint, queryParams); return ( { // Allows for button to be clickable in a clickable row diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index 3aafa34a73..be03fc5b53 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -4,7 +4,7 @@ import { Row } from "react-table"; import PATHS from "router/paths"; import { InjectedRouter } from "react-router"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { ISoftwareResponse } from "interfaces/software"; import { ITableQueryData } from "components/TableContainer/TableContainer"; @@ -57,11 +57,10 @@ const Software = ({ const tableHeaders = useMemo(() => generateTableHeaders(teamId), [teamId]); const handleRowSelect = (row: IRowProps) => { - const queryParams = { software_id: row.original.id, team_id: teamId }; - - const path = queryParams - ? `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams(queryParams)}` - : PATHS.MANAGE_HOSTS; + const path = getPathWithQueryParams(PATHS.MANAGE_HOSTS, { + software_id: row.original.id, + team_id: teamId, + }); router.push(path); }; diff --git a/frontend/pages/DashboardPage/cards/TotalHosts/TotalHosts.tsx b/frontend/pages/DashboardPage/cards/TotalHosts/TotalHosts.tsx index 8c0593b644..6447a0a10e 100644 --- a/frontend/pages/DashboardPage/cards/TotalHosts/TotalHosts.tsx +++ b/frontend/pages/DashboardPage/cards/TotalHosts/TotalHosts.tsx @@ -1,7 +1,7 @@ import React from "react"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import HostCountCard from "../HostCountCard"; @@ -22,14 +22,12 @@ const TotalHosts = ({ }: ITotalHostsProps): JSX.Element => { // build the manage hosts URL filtered by low disk space only // currently backend cannot filter by both low disk space and label - const queryParams = { - team_id: currentTeamId, - }; - const queryString = buildQueryStringFromParams(queryParams); const endpoint = selectedPlatformLabelId ? PATHS.MANAGE_HOSTS_LABEL(selectedPlatformLabelId) : PATHS.MANAGE_HOSTS; - const path = `${endpoint}?${queryString}`; + const path = getPathWithQueryParams(endpoint, { + team_id: currentTeamId, + }); return ( ); }; @@ -93,9 +94,9 @@ const PlatformHostCounts = ({ iconName="windows" count={windowsCount} title="Windows" - path={PATHS.MANAGE_HOSTS_LABEL(windowsLabelId).concat( - teamId !== undefined ? `?team_id=${teamId}` : "" - )} + path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(windowsLabelId), { + team_id: teamId, + })} /> ); }; @@ -115,9 +116,9 @@ const PlatformHostCounts = ({ iconName="linux" count={linuxCount} title="Linux" - path={PATHS.MANAGE_HOSTS_LABEL(linuxLabelId).concat( - teamId !== undefined ? `?team_id=${teamId}` : "" - )} + path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(linuxLabelId), { + team_id: teamId, + })} /> ); }; @@ -138,9 +139,9 @@ const PlatformHostCounts = ({ iconName="chrome" count={chromeCount} title="Chromebooks" - path={PATHS.MANAGE_HOSTS_LABEL(chromeLabelId).concat( - teamId !== undefined ? `?team_id=${teamId}` : "" - )} + path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(chromeLabelId), { + team_id: teamId, + })} /> ); }; @@ -161,9 +162,9 @@ const PlatformHostCounts = ({ iconName="iOS" count={iosCount} title="iPhones" - path={PATHS.MANAGE_HOSTS_LABEL(iosLabelId).concat( - teamId !== undefined ? `?team_id=${teamId}` : "" - )} + path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(iosLabelId), { + team_id: teamId, + })} /> ); }; @@ -184,9 +185,9 @@ const PlatformHostCounts = ({ iconName="iPadOS" count={ipadosCount} title="iPads" - path={PATHS.MANAGE_HOSTS_LABEL(ipadosLabelId).concat( - teamId !== undefined ? `?team_id=${teamId}` : "" - )} + path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(ipadosLabelId), { + team_id: teamId, + })} /> ); }; diff --git a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregate.tsx b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregate.tsx index 51de764120..eb7ae767d5 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregate.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/ProfileStatusAggregate/ProfileStatusAggregate.tsx @@ -1,7 +1,7 @@ import React from "react"; import paths from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { MdmProfileStatus } from "interfaces/mdm"; import { HOSTS_QUERY_PARAMS } from "services/entities/hosts"; import { ProfileStatusSummaryResponse } from "services/entities/mdm"; @@ -33,12 +33,12 @@ const ProfileStatusCount = ({ hostCount, tooltipText, }: IProfileStatusCountProps) => { - const linkHostsByStatus = `${paths.MANAGE_HOSTS}?${buildQueryStringFromParams( - { - team_id: teamId, - [HOSTS_QUERY_PARAMS.OS_SETTINGS]: statusValue, - } - )}`; + const hostsByStatusParams = { + team_id: teamId, + [HOSTS_QUERY_PARAMS.OS_SETTINGS]: statusValue, + }; + + const path = getPathWithQueryParams(paths.MANAGE_HOSTS, hostsByStatusParams); return (
@@ -49,7 +49,7 @@ const ProfileStatusCount = ({ layout="vertical" valueClassName={`${baseClass}__status-indicator-value`} /> - {hostCount} hosts + {hostCount} hosts
); }; diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx index ae267d3ad1..8b6d6fe5cb 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/components/DiskEncryptionTable/DiskEncryptionTable.tsx @@ -5,7 +5,7 @@ import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import diskEncryptionAPI, { IDiskEncryptionSummaryResponse, @@ -60,8 +60,8 @@ const DiskEncryptionTable = ({ [HOSTS_QUERY_PARAMS.DISK_ENCRYPTION]: status?.value, team_id: teamId, }; - const endpoint = PATHS.MANAGE_HOSTS; - const path = `${endpoint}?${buildQueryStringFromParams(queryParams)}`; + const path = getPathWithQueryParams(PATHS.MANAGE_HOSTS, queryParams); + router.push(path); }, [router] diff --git a/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx index 8ee18058ad..c70cbcde9f 100644 --- a/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx +++ b/frontend/pages/ManageControlsPage/Scripts/components/EditScriptModal/EditScriptModal.tsx @@ -2,6 +2,8 @@ import React, { useContext, useState } from "react"; import { useQuery } from "react-query"; import { NotificationContext } from "context/notification"; +import { AppContext } from "context/app"; +import { getPathWithQueryParams } from "utilities/url"; import scriptAPI from "services/entities/scripts"; import Button from "components/buttons/Button"; @@ -38,6 +40,7 @@ const EditScriptModal = ({ onExit, }: IEditScriptModal) => { const { renderFlash } = useContext(NotificationContext); + const { currentTeam } = useContext(AppContext); // Editable script content const [scriptFormData, setScriptFormData] = useState(""); @@ -115,11 +118,23 @@ const EditScriptModal = ({ />
To run this script on a host, go to the{" "} - page and select - a host. + {" "} + page and select a host.
To run the script across multiple hosts, add a policy automation on - the page. + the{" "} + {" "} + page.
void; + teamIdForApi?: number; } const ScriptDetailsModal = ({ @@ -75,6 +77,7 @@ const ScriptDetailsModal = ({ isScriptContentError, isHidden = false, onClickRunDetails, + teamIdForApi, }: IScriptDetailsModalProps) => { // For scrollable modal const [isTopScrolling, setIsTopScrolling] = useState(false); @@ -272,11 +275,23 @@ const ScriptDetailsModal = ({ {runScriptHelpText && (
To run this script on a host, go to the{" "} - page and select - a host. + {" "} + page and select a host.
To run the script across multiple hosts, add a policy automation on - the page. + the{" "} + {" "} + page.
)}
diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx index fa326a9a5d..08ff83270a 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import { Location } from "history"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { QueryContext } from "context/query"; import useToggleSidePanel from "hooks/useToggleSidePanel"; import { APP_CONTEXT_NO_TEAM_ID } from "interfaces/team"; @@ -73,11 +73,9 @@ const SoftwareAddPage = ({ (i: number): void => { setSidePanelOpen(false); // Only query param to persist between tabs is team id - const teamIdParam = buildQueryStringFromParams({ + const navPath = getPathWithQueryParams(addSoftwareSubNav[i].pathname, { team_id: location.query.team_id, }); - - const navPath = addSoftwareSubNav[i].pathname.concat(`?${teamIdParam}`); router.replace(navPath); }, [location.query.team_id, router, setSidePanelOpen] @@ -88,9 +86,9 @@ const SoftwareAddPage = ({ // is not provieded. if (!location.query.team_id) { router.replace( - `${location.pathname}?${buildQueryStringFromParams({ + getPathWithQueryParams(location.pathname, { team_id: APP_CONTEXT_NO_TEAM_ID, - })}` + }) ); return null; } @@ -99,9 +97,9 @@ const SoftwareAddPage = ({ setSelectedOsqueryTable(tableName); }; - const backUrl = `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ + const backUrl = getPathWithQueryParams(PATHS.SOFTWARE_TITLES, { team_id: location.query.team_id, - })}`; + }); return ( <> diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx index b5c8280c0c..6aabab0e70 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAppStoreVpp/SoftwareAppStoreVpp.tsx @@ -22,7 +22,7 @@ import Spinner from "components/Spinner"; import PremiumFeatureMessage from "components/PremiumFeatureMessage"; import Button from "components/buttons/Button"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import SoftwareVppForm from "./SoftwareVppForm"; import { getErrorMessage, teamHasVPPToken } from "./helpers"; import { ISoftwareVppFormData } from "./SoftwareVppForm/SoftwareVppForm"; @@ -150,9 +150,7 @@ const SoftwareAppStoreVpp = ({ ...(showAvailableForInstallOnly && { available_for_install: true }), }; - router.push( - `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(queryParams)}` - ); + router.push(getPathWithQueryParams(PATHS.SOFTWARE_TITLES, queryParams)); }; const onAddSoftware = async (formData: ISoftwareVppFormData) => { diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx index 2196f25058..e7d5e26619 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareCustomPackage/SoftwareCustomPackage.tsx @@ -5,7 +5,7 @@ import { useQuery } from "react-query"; import PATHS from "router/paths"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; import { getFileDetails, IFileDetails } from "utilities/file/fileUtils"; -import { buildQueryStringFromParams, QueryParams } from "utilities/url"; +import { getPathWithQueryParams, QueryParams } from "utilities/url"; import softwareAPI, { MAX_FILE_SIZE_BYTES, MAX_FILE_SIZE_MB, @@ -84,9 +84,9 @@ const SoftwareCustomPackage = ({ const onCancel = () => { router.push( - `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ + getPathWithQueryParams(PATHS.SOFTWARE_TITLES, { team_id: currentTeamId, - })}` + }) ); }; @@ -130,7 +130,7 @@ const SoftwareCustomPackage = ({ newQueryParams.available_for_install = true; } router.push( - `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams(newQueryParams)}` + getPathWithQueryParams(PATHS.SOFTWARE_TITLES, newQueryParams) ); renderFlash( diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx index 0d351168a2..1cbabcbfed 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx @@ -6,7 +6,7 @@ import { InjectedRouter } from "react-router"; import { useErrorHandler } from "react-error-boundary"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; import softwareAPI from "services/entities/software"; import labelsAPI, { getCustomLabels } from "services/entities/labels"; @@ -180,9 +180,10 @@ const FleetMaintainedAppDetailsPage = ({ setShowAppDetailsModal(true); }; - const backToAddSoftwareUrl = `${ - PATHS.SOFTWARE_ADD_FLEET_MAINTAINED - }?${buildQueryStringFromParams({ team_id: teamId })}`; + const backToAddSoftwareUrl = getPathWithQueryParams( + PATHS.SOFTWARE_ADD_FLEET_MAINTAINED, + { team_id: teamId } + ); const onCancel = () => { router.push(backToAddSoftwareUrl); @@ -206,11 +207,12 @@ const FleetMaintainedAppDetailsPage = ({ titleId = res.software_title_id; router.push( - `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ + getPathWithQueryParams(PATHS.SOFTWARE_TITLES, { team_id: teamId, available_for_install: true, - })}` + }) ); + renderFlash( "success", <> diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx index 29aae9ea0c..40dac18ee7 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; import { ISoftwareFleetMaintainedAppsResponse } from "services/entities/software"; import { getNextLocationPath } from "utilities/helpers"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { IFleetMaintainedApp } from "interfaces/software"; import TableContainer from "components/TableContainer"; @@ -123,11 +123,10 @@ const FleetMaintainedAppsTable = ({ ); const handleRowClick = (row: IRowProps) => { - const path = `${PATHS.SOFTWARE_FLEET_MAINTAINED_DETAILS( - row.original.id - )}?${buildQueryStringFromParams({ - team_id: teamId, - })}`; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_FLEET_MAINTAINED_DETAILS(row.original.id), + { team_id: teamId } + ); router.push(path); }; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx index 42f2012a87..55500fc87a 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx @@ -6,7 +6,7 @@ import PATHS from "router/paths"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import { APPLE_PLATFORM_DISPLAY_NAMES } from "interfaces/platform"; import { IFleetMaintainedApp } from "interfaces/software"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import TextCell from "components/TableContainer/DataTable/TextCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -31,11 +31,10 @@ export const generateTableConfig = ( Cell: (cellProps: ITableStringCellProps) => { const { name, id } = cellProps.row.original; - const path = `${PATHS.SOFTWARE_FLEET_MAINTAINED_DETAILS( - id - )}?${buildQueryStringFromParams({ - team_id: teamId, - })}`; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_FLEET_MAINTAINED_DETAILS(id), + { team_id: teamId } + ); return ; }, diff --git a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx index 633a6d98c4..81b928efdb 100644 --- a/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareOS/SoftwareOSTable/SoftwareOSTable.tsx @@ -21,7 +21,7 @@ import EmptySoftwareTable from "pages/SoftwarePage/components/EmptySoftwareTable import { IOSVersionsResponse } from "services/entities/operating_systems"; import generateTableConfig from "pages/DashboardPage/cards/OperatingSystems/OSTableConfig"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { getNextLocationPath } from "utilities/helpers"; import { SelectedPlatform } from "interfaces/platform"; @@ -162,12 +162,10 @@ const SoftwareOSTable = ({ }, [data, router, teamId]); const handleRowSelect = (row: IRowProps) => { - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamId, - }); - const path = `${PATHS.SOFTWARE_OS_DETAILS( - Number(row.original.os_version_id) - )}?${teamQueryParam}`; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_OS_DETAILS(Number(row.original.os_version_id)), + { team_id: teamId } + ); router.push(path); }; diff --git a/frontend/pages/SoftwarePage/SoftwarePage.tsx b/frontend/pages/SoftwarePage/SoftwarePage.tsx index 0beceae5c3..635d2c48bb 100644 --- a/frontend/pages/SoftwarePage/SoftwarePage.tsx +++ b/frontend/pages/SoftwarePage/SoftwarePage.tsx @@ -23,8 +23,8 @@ import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; import useTeamIdParam from "hooks/useTeamIdParam"; import { - buildQueryStringFromParams, convertParamsToSnakeCase, + getPathWithQueryParams, } from "utilities/url"; import { getNextLocationPath } from "utilities/helpers"; @@ -299,7 +299,9 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { setShowAddSoftwareModal(true); } else { router.push( - `${PATHS.SOFTWARE_ADD_FLEET_MAINTAINED}?team_id=${currentTeamId}` + getPathWithQueryParams(PATHS.SOFTWARE_ADD_FLEET_MAINTAINED, { + team_id: currentTeamId, + }) ); } }, [currentTeamId, router]); @@ -345,12 +347,16 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => { setResetPageIndex(true); // Fixes flakey page reset in table state when switching between tabs // Only query param to persist between tabs is team id - const teamIdParam = buildQueryStringFromParams({ + const teamIdParam = { team_id: location?.query.team_id, page: 0, // Fixes flakey page reset in API call when switching between tabs - }); + }; + + const navPath = getPathWithQueryParams( + softwareSubNav[i].pathname, + teamIdParam + ); - const navPath = softwareSubNav[i].pathname.concat(`?${teamIdParam}`); router.replace(navPath); }, [location, router] diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/AutomaticInstallModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/AutomaticInstallModal.tsx index f056ad34ce..9f9889382c 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/AutomaticInstallModal.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/AutomaticInstallModal/AutomaticInstallModal.tsx @@ -1,11 +1,13 @@ import React from "react"; +import { Link } from "react-router"; +import paths from "router/paths"; import { ISoftwareInstallPolicy } from "interfaces/software"; +import { getPathWithQueryParams } from "utilities/url"; import Modal from "components/Modal"; import Button from "components/buttons/Button"; import CustomLink from "components/CustomLink"; -import { Link } from "react-router"; const baseClass = "automatic-install-modal"; @@ -17,7 +19,13 @@ interface IPoliciesListItemProps { const PoliciesListItem = ({ teamId, policy }: IPoliciesListItemProps) => { return (
  • - {policy.name} + + {policy.name} +
  • ); }; diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx index 8d49bd5292..da85f13f13 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareInstallerCard/SoftwareInstallerCard.tsx @@ -10,7 +10,7 @@ import { } from "interfaces/software"; import softwareAPI from "services/entities/software"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import Card from "components/Card"; @@ -101,11 +101,11 @@ const InstallerStatusCount = ({ teamId, }: IInstallerStatusCountProps) => { const displayData = STATUS_DISPLAY_OPTIONS[status]; - const linkUrl = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ + const linkUrl = getPathWithQueryParams(PATHS.MANAGE_HOSTS, { software_title_id: softwareId, software_status: status, team_id: teamId, - })}`; + }); return ( ; } const { id } = cellProps.row.original; - const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); - const softwareVersionDetailsPath = `${PATHS.SOFTWARE_VERSION_DETAILS( - id.toString() - )}?${teamQueryParam}`; + const softwareVersionDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VERSION_DETAILS(id.toString()), + { team_id: teamId } + ); return ( { if (row.original.id) { - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamId, - }); - - const path = `${PATHS.SOFTWARE_TITLE_DETAILS( - row.original.id.toString() - )}?${teamQueryParam}`; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_TITLE_DETAILS(row.original.id.toString()), + { team_id: teamId } + ); router.push(path); } diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx index 5082286681..0ca69132b4 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig.tsx @@ -9,7 +9,7 @@ import { } from "interfaces/software"; import PATHS from "router/paths"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { IHeaderProps, IStringCellProps } from "interfaces/datatable_config"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; @@ -62,10 +62,10 @@ const getSoftwareNameCellData = ( softwareTitle: ISoftwareTitle, teamId?: number ) => { - const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); - const softwareTitleDetailsPath = `${PATHS.SOFTWARE_TITLE_DETAILS( - softwareTitle.id.toString() - )}?${teamQueryParam}`; + const softwareTitleDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_TITLE_DETAILS(softwareTitle.id.toString()), + { team_id: teamId } + ); const { software_package, app_store_app } = softwareTitle; let hasPackage = false; diff --git a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx index 0be57bf962..b46b0419ad 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareVersionsTableConfig.tsx @@ -2,7 +2,7 @@ import React from "react"; import { CellProps, Column } from "react-table"; import { InjectedRouter } from "react-router"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { formatSoftwareType, ISoftwareVersion, @@ -45,12 +45,12 @@ const generateTableHeaders = ( Cell: (cellProps: ITableStringCellProps) => { const { id, name, source } = cellProps.row.original; - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamId, - }); - const softwareVersionDetailsPath = `${PATHS.SOFTWARE_VERSION_DETAILS( - id.toString() - )}?${teamQueryParam}`; + const softwareVersionDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VERSION_DETAILS(id.toString()), + { + team_id: teamId, + } + ); return ( { if (row.original.cve) { const cveName = row.original.cve.toString(); - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamId, - }); - const softwareVulnerabilityDetailsPath = `${PATHS.SOFTWARE_VULNERABILITY_DETAILS( - cveName - )}?${teamQueryParam}`; + const softwareVulnerabilityDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VULNERABILITY_DETAILS(cveName), + { + team_id: teamId, + } + ); router.push(softwareVulnerabilityDetailsPath); } diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx index 92881a3c47..36660acc4e 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilities/SoftwareVulnerabilitiesTable/VulnerabilitiesTableConfig.tsx @@ -4,7 +4,7 @@ import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; import { formatSeverity } from "utilities/helpers"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { formatOperatingSystemDisplayName } from "interfaces/operating_system"; import { IVulnerability } from "interfaces/vulnerability"; @@ -78,10 +78,10 @@ const generateTableHeaders = ( const { cve } = cellProps.row.original; - const teamQueryParam = buildQueryStringFromParams({ team_id: teamId }); - const softwareVulnerabilitiesDetailsPath = `${PATHS.SOFTWARE_VULNERABILITY_DETAILS( - cve - )}?${teamQueryParam}`; + const softwareVulnerabilitiesDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VULNERABILITY_DETAILS(cve), + { team_id: teamId } + ); const onClickVulnerability = (e: React.MouseEvent) => { // Allows for button to be clickable in a clickable row diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx index 342192f973..1565c492bd 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SoftwareVulnOSVersions.tsx @@ -5,6 +5,7 @@ import { Row } from "react-table"; import { InjectedRouter } from "react-router"; import PATHS from "router/paths"; +import { getPathWithQueryParams } from "utilities/url"; import { IVulnerabilityResponse } from "services/entities/vulnerabilities"; import Card from "components/Card"; import TableContainer from "components/TableContainer"; @@ -42,11 +43,10 @@ const SoftwareVulnOSVersions = ({ if (row.original.os_version_id) { const softwareOsVersionId = Number(row.original.os_version_id); - const endpoint = PATHS.SOFTWARE_OS_DETAILS(softwareOsVersionId); - // since No Teams not supported on this page, falsiness of 0 is okay - const softwareOsDetailsPath = teamIdForApi - ? `${endpoint}?team_id=${teamIdForApi}` - : endpoint; + const softwareOsDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_OS_DETAILS(softwareOsVersionId), + { team_id: teamIdForApi } + ); router.push(softwareOsDetailsPath); } diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx index 2ac119dd25..10007a56b5 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnOSVersions/SwVulnOSTableConfig.tsx @@ -15,6 +15,7 @@ import { INumberCellProps, IStringCellProps, } from "interfaces/datatable_config"; +import { getPathWithQueryParams } from "utilities/url"; type ISWVulnTableColumnConfig = Column; @@ -34,11 +35,12 @@ const generateColumnConfigs = ( accessor: "name_only", Cell: ({ row }: ITableStringCellProps) => { const { name, os_version_id, platform } = row.original; - const endpoint = PATHS.SOFTWARE_OS_DETAILS(os_version_id); // since No Teams not supported on this page, falsiness of 0 is okay - const path = teamIdForApi - ? `${endpoint}?team_id=${teamIdForApi}` - : endpoint; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_OS_DETAILS(os_version_id), + { team_id: teamIdForApi } + ); + return ( { if (row.original.id) { const softwareVersionId = row.original.id; - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamIdForApi, - }); - const endpoint = PATHS.SOFTWARE_VERSION_DETAILS( - softwareVersionId.toString() - ); - // since No Teams not supported on this page, falsiness of 0 is okay - const softwareVersionDetailsPath = teamIdForApi - ? `${endpoint}?${teamQueryParam}` - : endpoint; + const softwareVersionDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VERSION_DETAILS(softwareVersionId.toString()), + { + team_id: teamIdForApi, + } + ); router.push(softwareVersionDetailsPath); } diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx index 3de15eebb3..9d94c43787 100644 --- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx +++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSoftwareVersions/SwVulnSwTableConfig.tsx @@ -11,6 +11,7 @@ import TextCell from "components/TableContainer/DataTable/TextCell"; import ViewAllHostsLink from "components/ViewAllHostsLink"; import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; import { InjectedRouter } from "react-router"; +import { getPathWithQueryParams } from "utilities/url"; type SwVulnTableColumnConfig = Column; @@ -33,11 +34,12 @@ const generateColumnConfigs = ( accessor: "name", Cell: ({ row }: ITableStringCellProps) => { const { name, id } = row.original; - const endpoint = PATHS.SOFTWARE_VERSION_DETAILS(id.toString()); // since No Teams not supported on this page, falsiness of 0 is okay - const path = teamIdForApi - ? `${endpoint}?team_id=${teamIdForApi}` - : endpoint; + const path = getPathWithQueryParams( + PATHS.SOFTWARE_VERSION_DETAILS(id.toString()), + { team_id: teamIdForApi } + ); + return ( { if (row.original.cve) { const cveName = row.original.cve.toString(); - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamIdForApi, - }); - const softwareVulnerabilityDetailsPath = `${PATHS.SOFTWARE_VULNERABILITY_DETAILS( - cveName - )}?${teamQueryParam}`; + const softwareVulnerabilityDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_VULNERABILITY_DETAILS(cveName), + { + team_id: teamIdForApi, + } + ); router.push(softwareVulnerabilityDetailsPath); } diff --git a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx index df0f85a698..34848e9ed0 100644 --- a/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx +++ b/frontend/pages/SoftwarePage/components/SoftwareVulnerabilitiesTable/SoftwareVulnerabilitiesTableConfig.tsx @@ -2,7 +2,7 @@ import React from "react"; import { InjectedRouter } from "react-router"; import { formatSeverity } from "utilities/helpers"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { ISoftwareVulnerability } from "interfaces/software"; import paths from "router/paths"; @@ -59,13 +59,11 @@ const generateTableConfig = ( Header: "Vulnerability", Cell: ({ cell: { value } }: ITextCellProps) => { const cveName = value.toString(); - const teamQueryParam = buildQueryStringFromParams({ - team_id: teamId, - }); - const softwareVulnerabilityDetailsPath = `${paths.SOFTWARE_VULNERABILITY_DETAILS( - cveName - )}?${teamQueryParam}`; + const softwareVulnerabilityDetailsPath = getPathWithQueryParams( + paths.SOFTWARE_VULNERABILITY_DETAILS(cveName), + { team_id: teamId } + ); return ( diff --git a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx index a3f82ade0f..114c6292a5 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Calendars/Calendars.tsx @@ -20,6 +20,7 @@ import PremiumFeatureMessage from "components/PremiumFeatureMessage/PremiumFeatu import Icon from "components/Icon"; import Card from "components/Card"; import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; +import { getPathWithQueryParams } from "utilities/url"; const CREATING_SERVICE_ACCOUNT = "https://www.fleetdm.com/learn-more-about/creating-service-accounts"; @@ -75,7 +76,7 @@ const baseClass = "calendars-integration"; const Calendars = (): JSX.Element => { const { renderFlash } = useContext(NotificationContext); - const { isPremiumTier } = useContext(AppContext); + const { currentTeam, isPremiumTier } = useContext(AppContext); const [formData, setFormData] = useState({ domain: "", @@ -422,7 +423,9 @@ const Calendars = (): JSX.Element => {

    Now head over to{" "} {" "} to finish setup. diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index dd5d715c00..b87069f8a8 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -982,6 +982,7 @@ const HostDetailsPage = ({ host={host} currentUser={currentUser} onCloseScriptModalGroup={onCloseScriptModalGroup} + teamIdForApi={currentTeam?.id} /> )} {!!host && showTransferHostModal && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/ScriptModalGroup/ScriptModalGroup.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/ScriptModalGroup/ScriptModalGroup.tsx index d6a6744d07..543d45597f 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/ScriptModalGroup/ScriptModalGroup.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/ScriptModalGroup/ScriptModalGroup.tsx @@ -20,6 +20,7 @@ interface IScriptsProps { currentUser: IUser | null; host: IHost; onCloseScriptModalGroup: () => void; + teamIdForApi?: number; } type ScriptGroupModals = @@ -33,6 +34,7 @@ const ScriptModalGroup = ({ currentUser, host, onCloseScriptModalGroup, + teamIdForApi, }: IScriptsProps) => { const [previousModal, setPreviousModal] = useState(null); const [currentModal, setCurrentModal] = useState( @@ -163,6 +165,7 @@ const ScriptModalGroup = ({ isScriptContentError={isSelectedScriptContentError} isHidden={currentModal !== "view-script"} showHostScriptActions + teamIdForApi={teamIdForApi} /> void; @@ -75,16 +74,20 @@ const SelectQueryModal = ({ const onQueryHostCustom = () => { setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE); router.push( - PATHS.NEW_QUERY() + - TAGGED_TEMPLATES.queryByHostRoute(hostId, currentTeamId) + getPathWithQueryParams(PATHS.NEW_QUERY, { + host_id: hostId, + team_id: currentTeamId, + }) ); }; const onQueryHostSaved = (selectedQuery: ISchedulableQuery) => { setSelectedQueryTargetsByType(DEFAULT_TARGETS_BY_TYPE); router.push( - PATHS.EDIT_QUERY(selectedQuery.id) + - TAGGED_TEMPLATES.queryByHostRoute(hostId, currentTeamId) + getPathWithQueryParams(PATHS.EDIT_QUERY(selectedQuery.id), { + host_id: hostId, + team_id: currentTeamId, + }) ); }; diff --git a/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx b/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx index 4ab02ae45f..13ca6264f8 100644 --- a/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx +++ b/frontend/pages/hosts/details/HostQueryReport/HostQueryReport.tsx @@ -1,21 +1,24 @@ -import BackLink from "components/BackLink"; -import Icon from "components/Icon"; -import MainContent from "components/MainContent"; -import ShowQueryModal from "components/modals/ShowQueryModal"; -import Spinner from "components/Spinner"; -import { AppContext } from "context/app"; -import { - IGetQueryResponse, - ISchedulableQuery, -} from "interfaces/schedulable_query"; import React, { useCallback, useContext, useState } from "react"; import { useQuery } from "react-query"; import { browserHistory, InjectedRouter, Link } from "react-router"; import { Params } from "react-router/lib/Router"; import PATHS from "router/paths"; +import { AppContext } from "context/app"; + +import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants"; +import { getPathWithQueryParams } from "utilities/url"; import hqrAPI, { IGetHQRResponse } from "services/entities/host_query_report"; import queryAPI from "services/entities/queries"; -import { DOCUMENT_TITLE_SUFFIX } from "utilities/constants"; +import { + IGetQueryResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; + +import BackLink from "components/BackLink"; +import Icon from "components/Icon"; +import MainContent from "components/MainContent"; +import ShowQueryModal from "components/modals/ShowQueryModal"; +import Spinner from "components/Spinner"; import HQRTable from "./HQRTable"; const baseClass = "host-query-report"; @@ -29,7 +32,7 @@ const HostQueryReport = ({ router, params: { host_id, query_id }, }: IHostQueryReportProps) => { - const { config } = useContext(AppContext); + const { config, currentTeam } = useContext(AppContext); const globalReportsDisabled = config?.server_settings.query_reports_disabled; const hostId = Number(host_id); const queryId = Number(query_id); @@ -105,7 +108,10 @@ const HostQueryReport = ({ } const HQRHeader = useCallback(() => { - const fullReportPath = PATHS.QUERY_DETAILS(queryId); + const fullReportPath = getPathWithQueryParams( + PATHS.QUERY_DETAILS(queryId), + { team_id: currentTeam?.id } + ); return (

    diff --git a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx index 70e70d515a..ec6e3fa0bb 100644 --- a/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx +++ b/frontend/pages/hosts/details/cards/Policies/HostPolicies.tsx @@ -3,10 +3,11 @@ import { InjectedRouter } from "react-router"; import { Row } from "react-table"; import { noop } from "lodash"; +import paths from "router/paths"; import { isAndroid } from "interfaces/platform"; import { IHostPolicy } from "interfaces/policy"; import { PolicyResponse, SUPPORT_LINK } from "utilities/constants"; -import { createHostsByPolicyPath } from "utilities/helpers"; +import { getPathWithQueryParams } from "utilities/url"; import TableContainer from "components/TableContainer"; import EmptyTable from "components/EmptyTable"; import Card from "components/Card"; @@ -61,13 +62,14 @@ const Policies = ({ (row: IHostPoliciesRowProps) => { const { id: policyId, response: policyResponse } = row.original; - const viewAllHostPath = createHostsByPolicyPath( - policyId, - policyResponse === "pass" - ? PolicyResponse.PASSING - : PolicyResponse.FAILING, - currentTeamId - ); + const viewAllHostPath = getPathWithQueryParams(paths.MANAGE_HOSTS, { + policy_id: policyId, + policy_response: + policyResponse === "pass" + ? PolicyResponse.PASSING + : PolicyResponse.FAILING, + team_id: currentTeamId, + }); router.push(viewAllHostPath); }, diff --git a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx index ebe9d09272..c4a2d2ff2c 100644 --- a/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/HostSoftwareTableConfig.tsx @@ -19,6 +19,7 @@ import { } from "interfaces/datatable_config"; import { IDropdownOption } from "interfaces/dropdownOption"; import PATHS from "router/paths"; +import { getPathWithQueryParams } from "utilities/url"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; @@ -183,8 +184,9 @@ export const generateSoftwareTableHeaders = ({ Cell: (cellProps: ITableStringCellProps) => { const { id, name, source, app_store_app } = cellProps.row.original; - const softwareTitleDetailsPath = PATHS.SOFTWARE_TITLE_DETAILS( - id.toString().concat(`?team_id=${teamId}`) + const softwareTitleDetailsPath = getPathWithQueryParams( + PATHS.SOFTWARE_TITLE_DETAILS(id.toString()), + { team_id: teamId } ); return ( diff --git a/frontend/pages/labels/NewLabelPage/DynamicLabel/DynamicLabel.tsx b/frontend/pages/labels/NewLabelPage/DynamicLabel/DynamicLabel.tsx index f22ef4c858..3afc357882 100644 --- a/frontend/pages/labels/NewLabelPage/DynamicLabel/DynamicLabel.tsx +++ b/frontend/pages/labels/NewLabelPage/DynamicLabel/DynamicLabel.tsx @@ -5,7 +5,7 @@ import PATHS from "router/paths"; import labelsAPI from "services/entities/labels"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { IApiError } from "interfaces/errors"; import DynamicLabelForm from "pages/labels/components/DynamicLabelForm"; @@ -37,9 +37,9 @@ const DynamicLabel = ({ .create(formData) .then((res) => { router.push( - `${PATHS.MANAGE_HOSTS_LABEL( - res.label.id - )}?${buildQueryStringFromParams({ team_id: currentTeam?.id })}` + getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(res.label.id), { + team_id: currentTeam?.id, + }) ); renderFlash("success", "Label added successfully."); }) diff --git a/frontend/pages/labels/NewLabelPage/ManualLabel/ManualLabel.tsx b/frontend/pages/labels/NewLabelPage/ManualLabel/ManualLabel.tsx index 5b3aee1c2d..6c15f726ee 100644 --- a/frontend/pages/labels/NewLabelPage/ManualLabel/ManualLabel.tsx +++ b/frontend/pages/labels/NewLabelPage/ManualLabel/ManualLabel.tsx @@ -5,7 +5,7 @@ import PATHS from "router/paths"; import labelsAPI from "services/entities/labels"; import { AppContext } from "context/app"; import { NotificationContext } from "context/notification"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import { IApiError } from "interfaces/errors"; import ManualLabelForm from "pages/labels/components/ManualLabelForm"; @@ -28,9 +28,9 @@ const ManualLabel = ({ router }: IManualLabelProps) => { .create(formData) .then((res) => { router.push( - `${PATHS.MANAGE_HOSTS_LABEL( - res.label.id - )}?${buildQueryStringFromParams({ team_id: currentTeam?.id })}` + getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(res.label.id), { + team_id: currentTeam?.id, + }) ); renderFlash("success", "Label added successfully."); }) diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx index 66e2faa46e..a4bad2eec9 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useMemo, useState } from "react"; import { useQuery } from "react-query"; import { omit } from "lodash"; +import paths from "router/paths"; import { IPolicyStats } from "interfaces/policy"; import { CommaSeparatedPlatformString, @@ -14,6 +15,7 @@ import softwareAPI, { ISoftwareTitlesResponse, } from "services/entities/software"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; +import { getPathWithQueryParams } from "utilities/url"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; @@ -296,8 +298,14 @@ const InstallSoftwareModal = ({
    No software available for install
    - Go to Software to - add software to this team. + Go to{" "} + {" "} + to add software to this team.
    ); diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx index cb6cf1da05..be80402a4f 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx @@ -11,9 +11,11 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import Icon from "components/Icon"; import { IPolicyStats } from "interfaces/policy"; import PATHS from "router/paths"; + +import { getPathWithQueryParams } from "utilities/url"; import sortUtils from "utilities/sort"; import { PolicyResponse } from "utilities/constants"; -import { createHostsByPolicyPath } from "utilities/helpers"; + import InheritedBadge from "components/InheritedBadge"; import { getConditionalSelectHeaderCheckboxProps } from "components/TableContainer/utilities/config_utils"; import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; @@ -104,47 +106,51 @@ const generateTableHeaders = ( /> ), accessor: "name", - Cell: (cellProps: ICellProps): JSX.Element => ( - -
    {cellProps.cell.value}
    - {isPremiumTier && cellProps.row.original.critical && ( -
    - - - - - This policy has been marked as critical. - -
    - )} - {viewingTeamPolicies && - cellProps.row.original.team_id === null && ( + Cell: (cellProps: ICellProps): JSX.Element => { + const { critical, id, team_id } = cellProps.row.original; + return ( + +
    {cellProps.cell.value}
    + {isPremiumTier && critical && ( +
    + + + + + This policy has been marked as critical. + +
    + )} + {viewingTeamPolicies && team_id === null && ( )} - - } - path={PATHS.EDIT_POLICY(cellProps.row.original)} - /> - ), + + } + path={getPathWithQueryParams(PATHS.EDIT_POLICY(id), { + team_id, + })} + /> + ); + }, sortType: "caseInsensitive", }, { @@ -157,37 +163,35 @@ const generateTableHeaders = ( ), accessor: "passing_host_count", Cell: (cellProps: ICellProps): JSX.Element => { - if (cellProps.row.original.has_run) { + const { has_run, id, next_update_ms } = cellProps.row.original; + + if (has_run) { return ( ); } return (
    - - --- - + --- - {getTooltip(cellProps.row.original.next_update_ms)} + {getTooltip(next_update_ms)}
    ); @@ -203,37 +207,35 @@ const generateTableHeaders = ( ), accessor: "failing_host_count", Cell: (cellProps: ICellProps): JSX.Element => { - if (cellProps.row.original.has_run) { + const { has_run, id, next_update_ms } = cellProps.row.original; + + if (has_run) { return ( ); } return (
    - - --- - + --- - {getTooltip(cellProps.row.original.next_update_ms)} + {getTooltip(next_update_ms)}
    ); diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx index 947a30bff9..5d748498f1 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PolicyRunScriptModal/PolicyRunScriptModal.tsx @@ -2,7 +2,9 @@ import React, { useCallback, useState } from "react"; import { useQuery } from "react-query"; import { omit } from "lodash"; +import paths from "router/paths"; import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; +import { getPathWithQueryParams } from "utilities/url"; import scriptsAPI, { IListScriptsQueryKey, @@ -183,9 +185,12 @@ const PolicyRunScriptModal = ({ No scripts available for install
    Go to{" "} - - Controls > Scripts - {" "} + {" "} to add scripts to this team.
    diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx index 48a39cb2c5..235f5b150d 100644 --- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx +++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx @@ -24,6 +24,7 @@ import teamPoliciesAPI from "services/entities/team_policies"; import hostAPI from "services/entities/hosts"; import statusAPI from "services/entities/status"; import { DOCUMENT_TITLE_SUFFIX, LIVE_POLICY_STEPS } from "utilities/constants"; +import { getPathWithQueryParams } from "utilities/url"; import QuerySidePanel from "components/side_panels/QuerySidePanel"; import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor"; @@ -215,7 +216,9 @@ const PolicyPage = ({ !(storedPolicy?.team_id?.toString() === location.query.team_id) ) { router.push( - `${location.pathname}?team_id=${storedPolicy?.team_id?.toString()}` + getPathWithQueryParams(location.pathname, { + team_id: storedPolicy?.team_id?.toString(), + }) ); } @@ -304,6 +307,7 @@ const PolicyPage = ({ goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]), onOpenSchemaSidebar, renderLiveQueryWarning, + teamIdForApi, }; const step2Opts = { diff --git a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx index 553209818e..4af79a18ec 100644 --- a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx +++ b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx @@ -10,6 +10,7 @@ import { NotificationContext } from "context/notification"; import PATHS from "router/paths"; import debounce from "utilities/debounce"; import deepDifference from "utilities/deep_difference"; +import { getPathWithQueryParams } from "utilities/url"; import { IPolicyFormData, IPolicy } from "interfaces/policy"; import BackLink from "components/BackLink"; @@ -30,6 +31,7 @@ interface IQueryEditorProps { goToSelectTargets: () => void; onOpenSchemaSidebar: () => void; renderLiveQueryWarning: () => JSX.Element | null; + teamIdForApi?: number; } const QueryEditor = ({ @@ -46,6 +48,7 @@ const QueryEditor = ({ goToSelectTargets, onOpenSchemaSidebar, renderLiveQueryWarning, + teamIdForApi, }: IQueryEditorProps): JSX.Element | null => { const { currentUser, isPremiumTier, filteredPoliciesPath } = useContext( AppContext @@ -161,7 +164,11 @@ const QueryEditor = ({ (data) => data.policy ); setIsUpdatingPolicy(false); - router.push(PATHS.EDIT_POLICY(policy)); + router.push( + getPathWithQueryParams(PATHS.EDIT_POLICY(policy.id), { + team_id: policy.team_id, + }) + ); renderFlash("success", "Policy created!"); } catch (createError: any) { console.error(createError); @@ -234,7 +241,12 @@ const QueryEditor = ({ // Function instead of constant eliminates race condition with filteredPoliciesPath const backToPoliciesPath = () => { - return filteredPoliciesPath || PATHS.MANAGE_POLICIES; + const queryParams = { team_id: teamIdForApi }; + + return ( + filteredPoliciesPath || + getPathWithQueryParams(PATHS.MANAGE_POLICIES, queryParams) + ); }; return ( diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index 558428939d..38be850b6b 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -7,7 +7,10 @@ import { AppContext } from "context/app"; import { QueryContext } from "context/query"; import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; +import { DEFAULT_QUERY } from "utilities/constants"; import { getPerformanceImpactDescription } from "utilities/helpers"; +import { getPathWithQueryParams } from "utilities/url"; + import { isQueryablePlatform, QueryablePlatform, @@ -22,7 +25,7 @@ import { DEFAULT_TARGETS_BY_TYPE } from "interfaces/target"; import { API_ALL_TEAMS_ID } from "interfaces/team"; import queriesAPI, { IQueriesResponse } from "services/entities/queries"; import PATHS from "router/paths"; -import { DEFAULT_QUERY } from "utilities/constants"; + import Button from "components/buttons/Button"; import TableDataError from "components/DataError"; import MainContent from "components/MainContent"; @@ -181,7 +184,9 @@ const ManageQueriesPage = ({ const onCreateQueryClick = useCallback(() => { setLastEditedQueryBody(DEFAULT_QUERY.query); - router.push(PATHS.NEW_QUERY(currentTeamId)); + router.push( + getPathWithQueryParams(PATHS.NEW_QUERY, { team_id: currentTeamId }) + ); }, [currentTeamId, router, setLastEditedQueryBody]); const toggleDeleteQueryModal = useCallback(() => { diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 3c5bb98d5c..cde2a68b61 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -2,17 +2,18 @@ import React, { useContext, useCallback, useMemo } from "react"; import { InjectedRouter } from "react-router"; import { Row } from "react-table"; +import { SingleValue } from "react-select-5"; +import PATHS from "router/paths"; import { AppContext } from "context/app"; import { IEmptyTableProps } from "interfaces/empty_table"; import { APP_CONTEXT_ALL_TEAMS_ID } from "interfaces/team"; import { isQueryablePlatform, SelectedPlatform } from "interfaces/platform"; import { IEnhancedQuery } from "interfaces/schedulable_query"; -import { ITableQueryData } from "components/TableContainer/TableContainer"; -import PATHS from "router/paths"; import { getNextLocationPath } from "utilities/helpers"; +import { getPathWithQueryParams } from "utilities/url"; -import { SingleValue } from "react-select-5"; +import { ITableQueryData } from "components/TableContainer/TableContainer"; import DropdownWrapper from "components/forms/fields/DropdownWrapper"; import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper"; import TableContainer from "components/TableContainer"; @@ -227,9 +228,11 @@ const QueriesTable = ({ const handleRowSelect = (row: IRowProps) => { if (row.original.id) { - const path = PATHS.QUERY_DETAILS(row.original.id, currentTeamId); - - router && router.push(path); + router?.push( + getPathWithQueryParams(PATHS.QUERY_DETAILS(row.original.id), { + team_id: currentTeamId, + }) + ); } }; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index b2208e632d..c698325893 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -7,19 +7,21 @@ import PATHS from "router/paths"; import { Tooltip as ReactTooltip5 } from "react-tooltip-5"; -import permissionsUtils from "utilities/permissions"; -import { IUser } from "interfaces/user"; import { secondsToDhms } from "utilities/helpers"; -import { - IEnhancedQuery, - ISchedulableQuery, -} from "interfaces/schedulable_query"; +import permissionsUtils from "utilities/permissions"; +import { getPathWithQueryParams } from "utilities/url"; + import { isScheduledQueryablePlatform, ScheduledQueryablePlatform, CommaSeparatedPlatformString, } from "interfaces/platform"; +import { + IEnhancedQuery, + ISchedulableQuery, +} from "interfaces/schedulable_query"; import { API_ALL_TEAMS_ID } from "interfaces/team"; +import { IUser } from "interfaces/user"; import Icon from "components/Icon"; import Checkbox from "components/forms/fields/Checkbox"; @@ -32,7 +34,6 @@ import PerformanceImpactCell from "components/TableContainer/DataTable/Performan import TooltipWrapper from "components/TooltipWrapper"; import InheritedBadge from "components/InheritedBadge"; import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper"; - import QueryAutomationsStatusIndicator from "../QueryAutomationsStatusIndicator"; interface IQueryRow { @@ -137,50 +138,49 @@ const generateColumnConfigs = ({ ), accessor: "name", Cell: (cellProps: ICellProps): JSX.Element => { + const { id, team_id, observer_can_run } = cellProps.row.original; return (
    {cellProps.cell.value}
    - {!isCurrentTeamObserverOrGlobalObserver && - cellProps.row.original.observer_can_run && ( -
    - - - - - Observers can run this query. - -
    - )} + {!isCurrentTeamObserverOrGlobalObserver && observer_can_run && ( +
    + + + + + Observers can run this query. + +
    + )} {viewingTeamScope && // inherited - cellProps.row.original.team_id !== currentTeamId && ( + team_id !== currentTeamId && ( )} } - path={PATHS.QUERY_DETAILS( - cellProps.row.original.id, - cellProps.row.original.team_id ?? currentTeamId - )} + path={getPathWithQueryParams(PATHS.QUERY_DETAILS(id), { + team_id: team_id ?? currentTeamId, + })} /> ); }, diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx index 123dc8fefd..b2a88ce54e 100644 --- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx +++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx @@ -22,7 +22,7 @@ import { isTeamObserver, } from "utilities/permissions/permissions"; import { DOCUMENT_TITLE_SUFFIX, SUPPORT_LINK } from "utilities/constants"; -import { buildQueryStringFromParams } from "utilities/url"; +import { getPathWithQueryParams } from "utilities/url"; import useTeamIdParam from "hooks/useTeamIdParam"; import Spinner from "components/Spinner/Spinner"; @@ -179,7 +179,9 @@ const QueryDetailsPage = ({ !(storedQuery?.team_id?.toString() === location.query.team_id) ) { router.push( - `${location.pathname}?team_id=${storedQuery?.team_id?.toString()}` + getPathWithQueryParams(location.pathname, { + team_id: storedQuery?.team_id?.toString(), + }) ); } @@ -249,9 +251,9 @@ const QueryDetailsPage = ({ const backToQueriesPath = () => { return ( filteredQueriesPath || - `${PATHS.MANAGE_QUERIES}?${buildQueryStringFromParams({ + getPathWithQueryParams(PATHS.MANAGE_QUERIES, { team_id: currentTeamId, - })}` + }) ); }; @@ -283,7 +285,11 @@ const QueryDetailsPage = ({