Merge branch 'main' into feat-23235-host-certificates

This commit is contained in:
Sarah Gillespie 2025-02-27 11:41:34 -06:00 committed by GitHub
commit f43fb9538a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
178 changed files with 2319 additions and 2292 deletions

View file

@ -74,6 +74,36 @@ How to view the disk encryption key:
> The disk encryption key is deleted if a host is transferred to a team with disk encryption turned off. To re-escrow they key, transfer the host back to a team with disk encryption on.
## Use disk encryption key to login
Disk encryption keys are used to login to workstations (hosts) when the end user forgets their password or when the host is returned to the organization after an end user leaves.
### macOS
1. With the macOS host in front of you, restart the host and select the end user's account.
2. Select the question mark icon **(?)** next to the password field and select **Restart and show password reset options**. If you don't see the **(?)** icon, try entering any incorrect password several times.
3. Follow the instructions on the Mac to enter the disk encryption (recovery) key.
### Linux
1. With the Linux host in front of you, restart it.
2. When prompted to unlock the disk, enter the disk encryption key.
3. On the **Host details** page in Fleet, find the local user's username in the **Users** table.
4. Next, add the following script to Fleet (deletes the local password (passphrase)):
```
passwd -d <username>
```
5. Head back to the **Host details** page and select **Actions > Run script** to run the script.
####
## Migrate macOS hosts
When migrating macOS hosts from another MDM solution, in order to complete the process of encrypting the hard drive and escrowing the key in Fleet, your end users must log out or restart their Mac.

View file

@ -0,0 +1 @@
- Fleet UI: Constistent behavior for table overflow and not hiding badges when user names overflow table cell

View file

@ -0,0 +1 @@
- Fleet UI: Fixed several links that were dropping team_id parameters resetting team to All teams

View file

@ -0,0 +1 @@
- add android mdm activities

View file

@ -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)
})

View file

@ -584,19 +584,7 @@ SELECT
'' AS vendor,
'' AS arch,
path AS installed_path
FROM cached_users CROSS JOIN firefox_addons USING (uid)
UNION
SELECT
name AS name,
version AS version,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
'' AS release,
'' AS vendor,
'' AS arch,
path AS installed_path
FROM python_packages;
FROM cached_users CROSS JOIN firefox_addons USING (uid);
```
## software_macos
@ -621,18 +609,6 @@ SELECT
path AS installed_path
FROM apps
UNION
SELECT
name AS name,
version AS version,
'' AS bundle_identifier,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
'' AS vendor,
0 AS last_opened_at,
path AS installed_path
FROM python_packages
UNION
SELECT
name AS name,
version AS version,
@ -757,6 +733,58 @@ WITH app_paths AS (
WHERE apps.bundle_identifier = 'org.mozilla.firefox'
```
## software_python_packages
- Description: Prior to osquery version 5.16.0, the python_packages table did not search user directories.
- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, endeavouros, manjaro, opensuse-leap, opensuse-tumbleweed, tuxedo, darwin, windows
- Discovery query:
```sql
SELECT 1 FROM osquery_info WHERE version_compare(version, '5.16.0') < 0
```
- Query:
```sql
SELECT
name AS name,
version AS version,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
'' AS vendor,
path AS installed_path
FROM python_packages
```
## software_python_packages_with_users_dir
- Description: As of osquery version 5.16.0, the python_packages table searches user directories with support from a cross join on users. See https://fleetdm.com/guides/osquery-consider-joining-against-the-users-table.
- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, endeavouros, manjaro, opensuse-leap, opensuse-tumbleweed, tuxedo, darwin, windows
- Discovery query:
```sql
SELECT 1 FROM osquery_info WHERE version_compare(version, '5.16.0') >= 0
```
- Query:
```sql
WITH cached_users AS (WITH cached_groups AS (select * from groups)
SELECT uid, username, type, groupname, shell
FROM users LEFT JOIN cached_groups USING (gid)
WHERE type <> 'special' AND shell NOT LIKE '%/false' AND shell NOT LIKE '%/nologin' AND shell NOT LIKE '%/shutdown' AND shell NOT LIKE '%/halt' AND username NOT LIKE '%$' AND username NOT LIKE '\_%' ESCAPE '\' AND NOT (username = 'sync' AND shell ='/bin/sync' AND directory <> ''))
SELECT
name AS name,
version AS version,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
'' AS vendor,
path AS installed_path
FROM cached_users CROSS JOIN python_packages USING (uid)
```
## software_vscode_extensions
- Platforms: linux, ubuntu, debian, rhel, centos, sles, kali, gentoo, amzn, pop, arch, linuxmint, void, nixos, endeavouros, manjaro, opensuse-leap, opensuse-tumbleweed, tuxedo, darwin, windows
@ -805,16 +833,6 @@ SELECT
install_location AS installed_path
FROM programs
UNION
SELECT
name AS name,
version AS version,
'' AS extension_id,
'' AS browser,
'python_packages' AS source,
'' AS vendor,
path AS installed_path
FROM python_packages
UNION
SELECT
name AS name,
version AS version,

View file

@ -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:

View file

@ -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 (
<div className={baseClass}>
<TabsWrapper>
<TabNav>
<Tabs
onSelect={(index) => setSelectedTabIndex(index)}
selectedIndex={selectedTabIndex}
@ -584,7 +585,7 @@ const PlatformWrapper = ({
// so we add a hidden pseudo element with the same text string
return (
<Tab key={navItem.name} data-text={navItem.name}>
{navItem.name}
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
@ -601,7 +602,7 @@ const PlatformWrapper = ({
);
})}
</Tabs>
</TabsWrapper>
</TabNav>
<div className="modal-cta-wrap">
<Button onClick={onCancel} variant="brand">
Done

View file

@ -3,7 +3,7 @@
.platform-wrapper {
padding: 0; // different to pad sticky subnav properly
.component__tabs-wrapper {
.tab-nav {
// negate problematic sticky positioning
position: initial;
.react-tabs {

View file

@ -1,5 +1,5 @@
import React from "react";
import * as DOMPurify from "dompurify";
import DOMPurify from "dompurify";
import classnames from "classnames";
interface IClickableUrls {

View file

@ -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<typeof CustomLink> = {
@ -24,3 +27,79 @@ export const ExternalLink: Story = {
newTab: true,
},
};
export const Multiline: Story = {
render: (args) => (
<div
style={{
width: "400px",
}}
>
Here&apos;s a CustomLink in a that might be split up across two lines{" "}
<CustomLink {...args} />
</div>
),
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) => (
<TooltipWrapper
tipContent={
<>
Tip content with a custom link <CustomLink {...args} />
</>
}
>
Hover to see custom link in tooltip{" "}
</TooltipWrapper>
),
args: {
url: "https://www.google.com",
text: "Tooltip link",
variant: "tooltip-link",
newTab: true,
},
};
export const BannerVariant: Story = {
render: (args) => (
<InfoBanner>
Here&apos;s a CustomLink in a banner <CustomLink {...args} />
</InfoBanner>
),
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&apos;t tab to this link even if you wanted to which is
useful when within a disabled component. <CustomLink {...args} />
</>
),
args: {
url: "https://www.google.com",
text: "Disabled Keyboard Navigation",
disableKeyboardNavigation: true,
},
};

View file

@ -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" : "";

View file

@ -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
}
}

View file

@ -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"
/>
}
>

View file

@ -25,29 +25,17 @@ import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams";
import { formatSelectedTargetsForApi } from "utilities/helpers";
import { capitalize } from "lodash";
import permissions from "utilities/permissions";
import {
LABEL_DISPLAY_MAP,
PlatformLabelNameFromAPI,
} from "utilities/constants";
import PageError from "components/DataError";
import TargetsInput from "components/LiveQuery/TargetsInput";
import Button from "components/buttons/Button";
import Spinner from "components/Spinner";
import TooltipWrapper from "components/TooltipWrapper";
import Icon from "components/Icon";
import SearchField from "components/forms/fields/SearchField";
import RevealButton from "components/buttons/RevealButton";
import TargetPillSelector from "./TargetChipSelector";
import { generateTableHeaders } from "./TargetsInput/TargetsInputHostsTableConfig";
interface ITargetPillSelectorProps {
entity: ISelectLabel | ISelectTeam;
isSelected: boolean;
onClick: (
value: ISelectLabel | ISelectTeam
) => React.MouseEventHandler<HTMLButtonElement>;
}
interface ISelectTargetsProps {
baseClass: string;
queryId?: number | null;
@ -86,11 +74,6 @@ const STALE_TIME = 60000;
const SECTION_CHARACTER_LIMIT = 600;
const isLabel = (entity: ISelectTargetsEntity) => "label_type" in entity;
const isBuiltInLabel = (
entity: ISelectTargetsEntity
): entity is ISelectLabel & { label_type: "builtin" } => {
return "label_type" in entity && entity.label_type === "builtin";
};
const isAllHosts = (entity: ISelectTargetsEntity) =>
"label_type" in entity &&
entity.name === "All Hosts" &&
@ -125,34 +108,6 @@ const getTruncatedEntityCount = (
return index;
};
const TargetPillSelector = ({
entity,
isSelected,
onClick,
}: ITargetPillSelectorProps): 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 (
<button
className="target-pill-selector"
data-selected={isSelected}
onClick={(e) => onClick(entity)(e)}
>
<Icon name={isSelected ? "check" : "plus"} />
<span className="selector-name">{displayText()}</span>
</button>
);
};
const SelectTargets = ({
baseClass,
queryId,

View file

@ -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<typeof TargetChipSelector> = {
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<typeof TargetChipSelector>;
// 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) => (
<TargetChipSelector
entity={args.entity}
isSelected={args.isSelected}
onClick={args.onClick}
/>
),
};
export const TeamExample: Story = {
args: {
entity: mockTeam,
isSelected: true,
onClick: (value) => (event) => {
event.preventDefault();
console.log("Clicked team:", value);
},
},
render: (args) => (
<TargetChipSelector
entity={args.entity}
isSelected={args.isSelected}
onClick={args.onClick}
/>
),
};
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) => (
<TargetChipSelector
entity={args.entity}
isSelected={args.isSelected}
onClick={args.onClick}
/>
),
};

View file

@ -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(
<TargetChipSelector
entity={mockLabel}
isSelected={false}
onClick={mockOnClick}
/>
);
expect(screen.getByText("Example Label")).toBeInTheDocument();
});
it("renders the correct display text for a team", () => {
render(
<TargetChipSelector
entity={mockTeam}
isSelected={false}
onClick={mockOnClick}
/>
);
expect(screen.getByText("Example Team")).toBeInTheDocument();
});
it("renders the correct icon when selected", () => {
render(
<TargetChipSelector entity={mockLabel} isSelected onClick={mockOnClick} />
);
expect(screen.getByLabelText("check")).toBeInTheDocument();
});
it("renders the correct icon when not selected", () => {
render(
<TargetChipSelector
entity={mockLabel}
isSelected={false}
onClick={mockOnClick}
/>
);
expect(screen.getByLabelText("plus")).toBeInTheDocument();
});
it("calls the onClick handler with the correct entity when clicked", () => {
render(
<TargetChipSelector
entity={mockLabel}
isSelected={false}
onClick={(value) => (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(
<TargetChipSelector entity={mockLabel} isSelected onClick={mockOnClick} />
);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("data-selected", "true");
});
it("applies the correct data-selected attribute when not selected", () => {
render(
<TargetChipSelector
entity={mockLabel}
isSelected={false}
onClick={mockOnClick}
/>
);
const button = screen.getByRole("button");
expect(button).toHaveAttribute("data-selected", "false");
});
});

View file

@ -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<HTMLButtonElement>;
}
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 (
<button
className="target-chip-selector"
data-selected={isSelected}
onClick={(e) => onClick(entity)(e)}
>
<Icon name={isSelected ? "check" : "plus"} />
<span className="selector-name">{displayText()}</span>
</button>
);
};
export default TargetChipSelector;

View file

@ -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 {

View file

@ -0,0 +1 @@
export { default } from "./TargetChipSelector";

View file

@ -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"
/>
}
>

View file

@ -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"
/>
}
>

View file

@ -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"
/>
}
>

View file

@ -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"
/>
}
>

View file

@ -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 (
<span className={`${baseClass}__install-software`}>

View file

@ -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 (
<span className="premium-icon-tip">
<span data-tip data-for={tipId}>
<Icon name="premium-feature" className="premium-feature-icon" />
</span>
<ReactTooltip
place={tooltipPlace ?? "top"}
type="dark"
effect="solid"
id={tipId}
backgroundColor={COLORS["tooltip-bg"]}
delayHide={tooltipDelayHide}
delayUpdate={500}
overridePosition={(pos: { left: number; top: number }) => {
return {
left: pos.left + leftAdj,
top: pos.top + topAdj,
};
}}
>
{`This is a Fleet Premium feature. `}
<CustomLink
url="https://fleetdm.com/upgrade"
text="Learn more"
newTab
multiline={false}
iconColor="core-fleet-white"
/>
</ReactTooltip>
</span>
);
};
export default PremiumFeatureIconWithTooltip;

View file

@ -1,14 +0,0 @@
import { Meta, StoryObj } from "@storybook/react";
import PremiumFeatureIconWithTooltip from "./PremiumFeatureIconWithTooltip";
const meta: Meta<typeof PremiumFeatureIconWithTooltip> = {
title: "Components/PremiumFeatureIconWithTooltip",
component: PremiumFeatureIconWithTooltip,
};
export default meta;
type Story = StoryObj<typeof PremiumFeatureIconWithTooltip>;
export const Basic: Story = {};

View file

@ -1,5 +0,0 @@
.premium-icon-tip {
font-style: normal;
font-size: $x-small;
font-weight: normal;
}

View file

@ -1 +0,0 @@
export { default } from "./PremiumFeatureIconWithTooltip";

View file

@ -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 (
<div className={baseClass}>
<p>Your Fleet Sandbox expires in {expiry}.</p>
<div className={`${baseClass}__tip`}>
<Icon name="lightbulb" size="large" />
<p>
<b>Quick tip: </b> Enroll a host to get started.
</p>
<form>
<Button
onClick={openAddHostModal}
className={`${baseClass}__add-hosts`}
variant="brand"
>
<span>Add hosts</span>
</Button>
</form>
</div>
</div>
);
}
return (
<a
href="https://fleetdm.com/docs/using-fleet/learn-how-to-use-fleet#how-to-add-your-device-to-fleet"
target="_blank"
rel="noreferrer"
className={baseClass}
>
<p>Your Fleet Sandbox expires in {expiry}.</p>
<p>
<b>Learn how to use Fleet</b>{" "}
<Icon name="external-link" color="core-fleet-black" />
</p>
</a>
);
};
export default SandboxExpiryMessage;

View file

@ -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;
}
}

View file

@ -1 +0,0 @@
export { default } from "./SandboxExpiryMessage";

View file

@ -84,19 +84,15 @@ const SoftwareOptionsSelector = ({
</Checkbox>
)}
{formData.automaticInstall && isCustomPackage && (
<InfoBanner
color="yellow"
cta={
<CustomLink
url={`${LEARN_MORE_ABOUT_BASE_LINK}/query-templates-for-automatic-software-install`}
text="Learn more"
newTab
/>
}
>
<InfoBanner color="yellow">
Installing software over existing installations might cause issues.
Fleet&apos;s policy may not detect these existing installations.
Please create a test team in Fleet to verify a smooth installation.
Please create a test team in Fleet to verify a smooth installation.{" "}
<CustomLink
url={`${LEARN_MORE_ABOUT_BASE_LINK}/query-templates-for-automatic-software-install`}
text="Learn more"
newTab
/>
</InfoBanner>
)}
</div>

View file

@ -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<typeof TabNav> = {
component: TabNav,
title: "Components/TabNav",
parameters: {
backgrounds: {
default: "light",
values: [
{
name: "light",
value: "#ffffff",
},
{
name: "dark",
value: "#333333",
},
],
},
},
};
export default meta;
type Story = StoryObj<typeof TabNav>;
export const Default: Story = {
render: () => {
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const platformSubNav = [
{ name: <TabText>Basic tab</TabText>, type: "type1" },
{ name: <TabText>Basic tab 2</TabText>, type: "type2" },
{
name: <TabText>Disabled tab</TabText>,
type: "type3",
disabled: true,
},
{ name: <TabText count={3}>Tab with count</TabText>, type: "type4" },
{
name: (
<TabText count={20} isErrorCount>
Tab with error count
</TabText>
),
type: "type5",
},
];
const renderPanel = (type: string) => {
switch (type) {
case "type1":
return <div>Content for Tab 1</div>;
case "type2":
return <div>Content for Tab 2</div>;
case "type3":
return <div>Content for Tab 3</div>;
case "type4":
return <div>Content for Tab 4</div>;
case "type5":
return <div>Content for Tab 5</div>;
default:
return null;
}
};
return (
<TabNav>
<Tabs
onSelect={(index) => setSelectedTabIndex(index)}
selectedIndex={selectedTabIndex}
>
<TabList>
{platformSubNav.map((navItem) => (
<Tab disabled={navItem.disabled}>
<TabText>{navItem.name}</TabText>
</Tab>
))}
</TabList>
{platformSubNav.map((navItem) => (
<TabPanel key={navItem.type}>
<div>{renderPanel(navItem.type)}</div>
</TabPanel>
))}
</Tabs>
</TabNav>
);
},
};

View file

@ -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(
<TabNav>
<Tabs>
<TabList>
<Tab>
<TabText>Tab 1</TabText>
</Tab>
<Tab>
<TabText>Tab 2</TabText>
</Tab>
</TabList>
<TabPanel>
<div>Content for Tab 1</div>
</TabPanel>
<TabPanel>
<div>Content for Tab 2</div>
</TabPanel>
</Tabs>
</TabNav>
);
// 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(
<TabNav>
<Tabs>
<TabList>
<Tab>
<TabText>Tab 1</TabText>
</Tab>
<Tab>
<TabText>Tab 2</TabText>
</Tab>
</TabList>
<TabPanel>
<div>Content for Tab 1</div>
</TabPanel>
<TabPanel>
<div>Content for Tab 2</div>
</TabPanel>
</Tabs>
</TabNav>
);
// 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();
});
});

View file

@ -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 <div className={classNames}>{children}</div>;
};
export default TabsWrapper;
export default TabNav;

View file

@ -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;
}
}
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./TabNav";

View file

@ -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 <div className={countClassNames}>{count.toLocaleString()}</div>;
}
return undefined;
};
return (
<div className={classNames}>
<div className={`${baseClass}__text}`} data-text={children}>
{children}
</div>
{renderCount()}
</div>
);
};
export default TabText;

View file

@ -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;
}
}
}

View file

@ -0,0 +1 @@
export { default } from "./TabText";

View file

@ -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 (
<div className={`${baseClass} ${baseClass}__${kebabCase(name)}`}>
{indicatePremiumFeature && (
<PremiumFeatureIconWithTooltip tooltipDelayHide={500} />
)}
<Button
disabled={indicatePremiumFeature}
onClick={() => onButtonClick(targetIds)}
variant={variant}
>
<Button onClick={() => onButtonClick(targetIds)} variant={variant}>
<>
{iconPosition === "left" && iconSvg && <Icon name={iconSvg} />}
{buttonText}

View file

@ -402,7 +402,6 @@ const DataTable = ({
hideButton,
iconSvg,
iconPosition,
indicatePremiumFeature,
} = actionButtonProps;
return (
<div className={`${baseClass}__${kebabCase(name)}`}>
@ -414,7 +413,6 @@ const DataTable = ({
targetIds={targetIds}
variant={variant}
hideButton={hideButton}
indicatePremiumFeature={indicatePremiumFeature}
iconSvg={iconSvg}
iconPosition={iconPosition}
/>

View file

@ -17,6 +17,8 @@ interface ITooltipTruncatedTextCellProps {
/** @deprecated use the prop `className` in order to add custom classes to this component */
classes?: string;
className?: string;
/** Content does not get truncated */
suffix?: React.ReactNode;
}
const baseClass = "tooltip-truncated-cell";
@ -27,13 +29,14 @@ const TooltipTruncatedTextCell = ({
tooltipBreakOnWord = false,
classes = "w250",
className,
suffix,
}: ITooltipTruncatedTextCellProps): JSX.Element => {
const classNames = classnames(baseClass, classes, className, {
"tooltip-break-on-word": tooltipBreakOnWord,
});
// Tooltip visibility logic: Enable only when text is truncated
const ref = useRef<HTMLInputElement>(null);
const ref = useRef<HTMLSpanElement>(null);
const [tooltipDisabled, setTooltipDisabled] = useState(true);
useLayoutEffect(() => {
@ -51,14 +54,14 @@ const TooltipTruncatedTextCell = ({
return (
<div className={classNames}>
<div
className="data-table__tooltip-truncated-text"
className="data-table__tooltip-truncated-text-container"
data-tip
data-for={tooltipId}
data-tip-disable={isDefaultValue || tooltipDisabled}
>
<span
ref={ref}
className={`data-table__tooltip-truncated-text--cell ${
className={`data-table__tooltip-truncated-text ${
isDefaultValue ? "text-muted" : ""
} ${tooltipDisabled ? "" : "truncated"}`}
>
@ -81,6 +84,7 @@ const TooltipTruncatedTextCell = ({
{/* Fixes triple click selecting next element in Safari */}
</>
</ReactTooltip>
{suffix && <span className="data-table__suffix">{suffix}</span>}
</div>
);
};

View file

@ -1,8 +1,22 @@
.tooltip-truncated-cell {
display: flex;
align-items: center; // For badges, etc
.text-muted {
color: $ui-fleet-black-50;
}
.data-table__tooltip-truncated-text-container {
display: flex; // Flex container for text and suffix
align-items: center; // Keep text and suffix aligned
min-width: 0; // Prevent flex child from growing beyond container
}
.data-table__tooltip-truncated-text {
white-space: nowrap; /* Prevent wrapping */
overflow: hidden; /* Hide overflowing text */
text-overflow: ellipsis; /* Add ellipsis for truncated text */
&--cell {
display: inline-block;
overflow: hidden;
@ -35,4 +49,9 @@
word-break: normal;
}
}
.data-table__suffix {
margin-left: 8px; /* Add spacing between text and suffix */
flex-shrink: 0; /* Prevent suffix from shrinking */
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -1 +0,0 @@
export { default } from "./TabsWrapper";

View file

@ -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 (
<Link

View file

@ -35,5 +35,9 @@
}
}
}
@media (min-width: $break-md) {
display: none;
}
}
}

View file

@ -1,26 +1,12 @@
import React, { KeyboardEvent } from "react";
import React from "react";
import { Meta, StoryFn } from "@storybook/react";
import { noop } from "lodash";
import AutoSizeInputField from ".";
import { IAutoSizeInputFieldProps } from "./AutoSizeInputField";
import "../../../../index.scss";
interface IAutoSizeInputFieldProps {
name: string;
placeholder: string;
value: string;
inputClassName?: string;
maxLength: number;
hasError?: boolean;
isDisabled?: boolean;
isFocused?: boolean;
onFocus: () => void;
onBlur: () => void;
onChange: (newSelectedValue: string) => void;
onKeyPress: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
}
export default {
component: AutoSizeInputField,
title: "Components/FormFields/Input/AutoSizeInputField",

View file

@ -1,7 +1,7 @@
import React, { KeyboardEvent, useEffect, useRef } from "react";
import classnames from "classnames";
interface IAutoSizeInputFieldProps {
export interface IAutoSizeInputFieldProps {
name: string;
placeholder: string;
value: string;

View file

@ -13,6 +13,7 @@ const Check = ({ color = "core-fleet-blue" }: ICheckProps) => {
height="16"
fill="none"
viewBox="0 0 16 16"
aria-label="check"
>
<path
stroke={COLORS[color]}

View file

@ -13,6 +13,7 @@ const Plus = ({ color = "core-fleet-blue" }: IPlusProps) => {
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-label="plus"
>
<path
d="M8 3v10M3 8h10"

View file

@ -2,7 +2,7 @@ import React, { useContext } from "react";
import { Link } from "react-router";
import classnames from "classnames";
import { QueryParams } from "utilities/url";
import { getPathWithQueryParams, QueryParams } from "utilities/url";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import { AppContext } from "context/app";
@ -170,7 +170,9 @@ const SiteTopNav = ({
const includeTeamId = (activePath: string) => {
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;
};

View file

@ -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 */

View file

@ -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);

View file

@ -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."

View file

@ -4,7 +4,8 @@ import { Row } from "react-table";
import { IMdmStatusCardData, IMdmSummaryMdmSolution } from "interfaces/mdm";
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";
@ -139,11 +140,15 @@ const Mdm = ({
</div>
)}
<div style={opacity}>
<TabsWrapper>
<TabNav>
<Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
<TabList>
<Tab>Solutions</Tab>
<Tab>Status</Tab>
<Tab>
<TabText>Solutions</TabText>
</Tab>
<Tab>
<TabText>Status</TabText>
</Tab>
</TabList>
<TabPanel>
{error ? (
@ -189,7 +194,7 @@ const Mdm = ({
)}
</TabPanel>
</Tabs>
</TabsWrapper>
</TabNav>
</div>
</div>
);

View file

@ -3,7 +3,7 @@
position: relative;
height: 100%; // centers loading spinner
.component__tabs-wrapper .table-container__header {
.tab-nav .table-container__header {
display: none;
}
@ -14,16 +14,6 @@
.status__header {
width: 30%;
}
.hosts__header {
border-right: 0;
padding-right: 0;
width: 60px;
}
.linkToFilteredHosts__header {
width: 140px;
}
}
}
}
@ -40,6 +30,5 @@
}
}
}
}
}

View file

@ -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 (
<HostCountCard

View file

@ -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 = ({
</div>
)}
<div style={opacity}>
<TabsWrapper>
<TabNav>
<Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
<TabList>
<Tab>Issues</Tab>
<Tab>Versions</Tab>
<Tab>
<TabText>Issues</TabText>
</Tab>
<Tab>
<TabText>Versions</TabText>
</Tab>
</TabList>
<TabPanel>
{errorMacAdmins ? (
@ -128,7 +133,7 @@ const Munki = ({
)}
</TabPanel>
</Tabs>
</TabsWrapper>
</TabNav>
</div>
</div>
);

View file

@ -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 {
@ -21,7 +21,6 @@
padding-right: 0;
}
.hosts_count__header {
border-right: 0;
padding-right: 0;
width: 9%;
}
@ -30,9 +29,6 @@
border-left: 0;
width: 40px;
}
.linkToFilteredHosts__header {
width: 140px;
}
}
tbody {

View file

@ -7,7 +7,7 @@ import React from "react";
import { CellProps, Column, HeaderProps } from "react-table";
import { InjectedRouter } from "react-router";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import PATHS from "router/paths";
import {
formatOperatingSystemDisplayName,
@ -67,12 +67,10 @@ const generateDefaultTableHeaders = (
const { name, os_version_id } = cellProps.row.original;
const teamQueryParam = buildQueryStringFromParams({
team_id: teamId,
});
const softwareOsDetailsPath = `${PATHS.SOFTWARE_OS_DETAILS(
os_version_id
)}?${teamQueryParam}`;
const softwareOsDetailsPath = getPathWithQueryParams(
PATHS.SOFTWARE_OS_DETAILS(os_version_id),
{ team_id: teamId }
);
const onClickSoftware = (e: React.MouseEvent) => {
// Allows for button to be clickable in a clickable row

View file

@ -4,11 +4,12 @@ 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";
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";
@ -56,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);
};
@ -76,11 +76,15 @@ const Software = ({
</div>
)}
<div style={opacity}>
<TabsWrapper>
<TabNav>
<Tabs selectedIndex={navTabIndex} onSelect={onTabChange}>
<TabList>
<Tab>All</Tab>
<Tab>Vulnerable</Tab>
<Tab>
<TabText>All</TabText>
</Tab>
<Tab>
<TabText>Vulnerable</TabText>
</Tab>
</TabList>
<TabPanel>
{!isSoftwareFetching && errorSoftware ? (
@ -129,7 +133,7 @@ const Software = ({
)}
</TabPanel>
</Tabs>
</TabsWrapper>
</TabNav>
</div>
</div>
);

View file

@ -19,7 +19,7 @@
.form-field--dropdown {
margin: 0;
}
.component__tabs-wrapper .table-container__header {
.tab-nav .table-container__header {
display: none;
}
&__empty-software {
@ -52,7 +52,6 @@
padding-right: 0;
}
.hosts_count__header {
border-right: 0;
padding-right: 0;
width: 60px;
}

View file

@ -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 (
<HostCountCard

View file

@ -1,6 +1,7 @@
import React, { useCallback } from "react";
import PATHS from "router/paths";
import { getPathWithQueryParams } from "utilities/url";
import { PLATFORM_NAME_TO_LABEL_NAME } from "pages/DashboardPage/helpers";
import { IHostSummary } from "interfaces/host_summary";
@ -71,9 +72,9 @@ const PlatformHostCounts = ({
iconName="darwin"
count={macCount}
title="macOS"
path={PATHS.MANAGE_HOSTS_LABEL(macLabelId).concat(
teamId !== undefined ? `?team_id=${teamId}` : ""
)}
path={getPathWithQueryParams(PATHS.MANAGE_HOSTS_LABEL(macLabelId), {
team_id: teamId,
})}
/>
);
};
@ -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,
})}
/>
);
};

View file

@ -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 (
<div>
<TabsWrapper>
<TabNav>
<Tabs
selectedIndex={getTabIndex(location?.pathname || "")}
onSelect={navigateToNav}
@ -121,13 +122,13 @@ const ManageControlsPage = ({
{controlsSubNav.map((navItem) => {
return (
<Tab key={navItem.name} data-text={navItem.name}>
{navItem.name}
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
</TabList>
</Tabs>
</TabsWrapper>
</TabNav>
{React.cloneElement(children, {
teamIdForApi,
currentPage: page,

View file

@ -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 (
<div className={`${baseClass}__profile-status-count`}>
@ -49,7 +49,7 @@ const ProfileStatusCount = ({
layout="vertical"
valueClassName={`${baseClass}__status-indicator-value`}
/>
<a href={linkHostsByStatus}>{hostCount} hosts</a>
<a href={path}>{hostCount} hosts</a>
</div>
);
};

View file

@ -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]

View file

@ -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 (
<div className={baseClass}>
<TabsWrapper>
<TabNav>
<Tabs
defaultIndex={platformByIndex.indexOf(selectedPlatform)}
onSelect={onTabChange}
>
<TabList>
{/* Bolding text when the tab is active causes a layout shift so
we add a hidden pseudo element with the same text string */}
<Tab key="macOS" data-text="macOS">
macOS
<TabText>macOS</TabText>
</Tab>
{isWindowsMdmEnabled && (
<Tab key="Windows" data-text="Windows">
Windows
<TabText>Windows</TabText>
</Tab>
)}
<Tab key="iOS" data-text="iOS">
iOS
<TabText>iOS</TabText>
</Tab>
<Tab key="iPadOS" data-text="iPadOS">
iPadOS
<TabText>iPadOS</TabText>
</Tab>
{isAndroidMdmEnabled && (
<Tab key="Android" data-text="Android">
@ -149,7 +148,7 @@ const PlatformTabs = ({
</TabPanel>
)}
</Tabs>
</TabsWrapper>
</TabNav>
</div>
);
};

View file

@ -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 = ({
/>
<div className="form-field__help-text">
To run this script on a host, go to the{" "}
<CustomLink text="Hosts" url={paths.MANAGE_HOSTS} /> page and select
a host.
<CustomLink
text="Hosts"
url={getPathWithQueryParams(paths.MANAGE_HOSTS, {
team_id: currentTeam?.id,
})}
/>{" "}
page and select a host.
<br />
To run the script across multiple hosts, add a policy automation on
the <CustomLink text="Policies" url={paths.MANAGE_POLICIES} /> page.
the{" "}
<CustomLink
text="Policies"
url={getPathWithQueryParams(paths.MANAGE_POLICIES, {
team_id: currentTeam?.id,
})}
/>{" "}
page.
</div>
</form>
<ModalFooter

View file

@ -32,6 +32,7 @@ import paths from "router/paths";
import ActionsDropdown from "components/ActionsDropdown";
import { generateActionDropdownOptions } from "pages/hosts/details/HostDetailsPage/modals/RunScriptModal/ScriptsTableConfig";
import GitOpsModeTooltipWrapper from "components/GitOpsModeTooltipWrapper";
import { getPathWithQueryParams } from "utilities/url";
const baseClass = "script-details-modal";
@ -58,6 +59,7 @@ interface IScriptDetailsModalProps {
isScriptContentError?: Error | null;
isHidden?: boolean;
onClickRunDetails?: (scriptExecutionId: string) => 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 && (
<div className="form-field__help-text">
To run this script on a host, go to the{" "}
<CustomLink text="Hosts" url={paths.MANAGE_HOSTS} /> page and select
a host.
<CustomLink
text="Hosts"
url={getPathWithQueryParams(paths.MANAGE_HOSTS, {
team_id: teamIdForApi,
})}
/>{" "}
page and select a host.
<br />
To run the script across multiple hosts, add a policy automation on
the <CustomLink text="Policies" url={paths.MANAGE_POLICIES} /> page.
the{" "}
<CustomLink
text="Policies"
url={getPathWithQueryParams(paths.MANAGE_POLICIES, {
team_id: teamIdForApi,
})}
/>{" "}
page.
</div>
)}
</div>

View file

@ -10,10 +10,6 @@
min-width: auto;
}
.data-table-block th:nth-last-child(2) {
border-right: 0;
}
@media (max-width: $break-lg) {
.view-hosts-link {
span {

View file

@ -51,8 +51,6 @@ const SetupAssistantProfileUploader = ({
text="Learn more"
className={`${baseClass}__new-tab`}
newTab
color="core-fleet-black"
iconColor="core-fleet-white"
/>
</>
);

View file

@ -4,14 +4,15 @@ 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";
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";
@ -72,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]
@ -87,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;
}
@ -98,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 (
<>
@ -112,7 +111,7 @@ const SoftwareAddPage = ({
className={`${baseClass}__back-to-software`}
/>
<h1>Add software</h1>
<TabsWrapper>
<TabNav>
<Tabs
selectedIndex={getTabIndex(location?.pathname || "")}
onSelect={navigateToNav}
@ -121,13 +120,13 @@ const SoftwareAddPage = ({
{addSoftwareSubNav.map((navItem) => {
return (
<Tab key={navItem.name} data-text={navItem.name}>
{navItem.name}
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
</TabList>
</Tabs>
</TabsWrapper>
</TabNav>
{React.cloneElement(children, {
router,
currentTeamId: parseInt(location.query.team_id, 10),

View file

@ -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) => {

View file

@ -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(

View file

@ -6,10 +6,9 @@ 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 teamPoliciesAPI from "services/entities/team_policies";
import labelsAPI, { getCustomLabels } from "services/entities/labels";
import { QueryContext } from "context/query";
import { AppContext } from "context/app";
@ -129,6 +128,7 @@ const FleetMaintainedAppDetailsPage = ({
}
const { renderFlash } = useContext(NotificationContext);
const handlePageError = useErrorHandler();
const { isPremiumTier } = useContext(AppContext);
const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext(
@ -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,22 +207,19 @@ 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",
<>
<b>{fleetApp?.name}</b> 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);
@ -236,46 +234,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",
// <>
// <b>{fleetApp?.name}</b> 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);
};

View file

@ -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);
};

View file

@ -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 <SoftwareNameCell name={name} path={path} router={router} />;
},

View file

@ -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);
};

View file

@ -23,15 +23,16 @@ 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";
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";
@ -298,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]);
@ -344,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]
@ -412,7 +419,7 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
const renderBody = () => {
return (
<div>
<TabsWrapper>
<TabNav>
<Tabs
selectedIndex={getTabIndex(location?.pathname || "")}
onSelect={navigateToNav}
@ -421,13 +428,13 @@ const SoftwarePage = ({ children, router, location }: ISoftwarePageProps) => {
{softwareSubNav.map((navItem) => {
return (
<Tab key={navItem.name} data-text={navItem.name}>
{navItem.name}
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
</TabList>
</Tabs>
</TabsWrapper>
</TabNav>
{React.cloneElement(children, {
router,
isSoftwareEnabled: Boolean(

View file

@ -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 (
<li key={policy.id} className={`${baseClass}__list-item`}>
<Link to={`/policies/${policy.id}?team_id=${teamId}`}>{policy.name}</Link>
<Link
to={getPathWithQueryParams(paths.EDIT_POLICY(policy.id), {
team_id: teamId,
})}
>
{policy.name}
</Link>
</li>
);
};

View file

@ -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 (
<DataSet

View file

@ -23,7 +23,7 @@ import {
APP_CONTEXT_ALL_TEAMS_ID,
APP_CONTEXT_NO_TEAM_ID,
} from "interfaces/team";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import Spinner from "components/Spinner";
@ -112,12 +112,12 @@ const SoftwareTitleDetailsPage = ({
return;
}
const queryParams: string = buildQueryStringFromParams({
team_id: teamIdForApi,
});
// redirect to software titles page if no versions are available
router.push(`${paths.SOFTWARE_TITLES}?${queryParams}`);
router.push(
getPathWithQueryParams(paths.SOFTWARE_TITLES, {
team_id: teamIdForApi,
})
);
}, [refetchSoftwareTitle, router, softwareTitle, teamIdForApi]);
const onTeamChange = useCallback(

View file

@ -7,7 +7,7 @@ import PATHS from "router/paths";
import { ISoftwareTitleVersion } from "interfaces/software";
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import TableContainer from "components/TableContainer";
import TableCount from "components/TableContainer/TableCount";
@ -92,12 +92,10 @@ const SoftwareTitleDetailsTable = ({
if (row.original.id) {
const softwareVersionId = row.original.id;
const teamQueryParam = buildQueryStringFromParams({
team_id: teamIdForApi,
});
const softwareVersionDetailsPath = `${PATHS.SOFTWARE_VERSION_DETAILS(
softwareVersionId.toString()
)}?${teamQueryParam}`;
const softwareVersionDetailsPath = getPathWithQueryParams(
PATHS.SOFTWARE_VERSION_DETAILS(softwareVersionId.toString()),
{ team_id: teamIdForApi }
);
router.push(softwareVersionDetailsPath);
}

View file

@ -6,7 +6,7 @@ import {
ISoftwareVulnerability,
} from "interfaces/software";
import PATHS from "router/paths";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import TextCell from "components/TableContainer/DataTable/TextCell";
import ViewAllHostsLink from "components/ViewAllHostsLink";
@ -63,10 +63,10 @@ const generateSoftwareTitleDetailsTableConfig = ({
return <TextCell />;
}
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 (
<LinkCell

View file

@ -1,7 +0,0 @@
.software-title-details-table {
.data-table {
.hosts_count__header {
border-right: 0;
}
}
}

View file

@ -11,8 +11,8 @@ import PATHS from "router/paths";
import { getNextLocationPath } from "utilities/helpers";
import { GITHUB_NEW_ISSUE_LINK } from "utilities/constants";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
getPathWithQueryParams,
} from "utilities/url";
import {
ISoftwareApiParams,
@ -248,13 +248,10 @@ const SoftwareTable = ({
const handleRowSelect = (row: IRowProps) => {
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);
}

View file

@ -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;

View file

@ -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 (
<SoftwareNameCell

View file

@ -53,11 +53,6 @@
width: $col-md;
}
.hosts_count__header {
width: auto;
border-right: 0;
}
@media (min-width: $break-lg) {
// expand the width of version header at larger screen sizes
.versions__header {

View file

@ -28,7 +28,7 @@ import {
IVulnerabilitiesResponse,
IVulnerabilitiesEmptyStateReason,
} from "services/entities/vulnerabilities";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import { getNextLocationPath } from "utilities/helpers";
import generateTableConfig from "./VulnerabilitiesTableConfig";
@ -187,13 +187,13 @@ const SoftwareVulnerabilitiesTable = ({
const handleRowSelect = (row: IRowProps) => {
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);
}

View file

@ -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

View file

@ -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);
}

View file

@ -15,6 +15,7 @@ import {
INumberCellProps,
IStringCellProps,
} from "interfaces/datatable_config";
import { getPathWithQueryParams } from "utilities/url";
type ISWVulnTableColumnConfig = Column<IVulnerabilityOSVersion>;
@ -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 (
<LinkCell
path={path}

View file

@ -8,7 +8,7 @@ import { Row } from "react-table";
import { IVulnerabilityResponse } from "services/entities/vulnerabilities";
import PATHS from "router/paths";
import { buildQueryStringFromParams } from "utilities/url";
import { getPathWithQueryParams } from "utilities/url";
import Card from "components/Card";
import TableContainer from "components/TableContainer";
@ -45,17 +45,13 @@ const SoftwareVulnSoftwareVersions = ({
const handleRowSelect = (row: IRowProps) => {
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);
}

View file

@ -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<IVulnerabilitySoftware>;
@ -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 (
<LinkCell
path={path}

View file

@ -57,7 +57,7 @@
}
&__wrapper {
.component__tabs-wrapper {
.tab-nav {
margin-bottom: $pad-xxlarge;
}
}

Some files were not shown because too many files have changed in this diff Show more