Merge branch 'main' into feat-automatic-migrate-windows-hosts
This commit is contained in:
@@ -1,3 +1,52 @@
|
||||
## Fleet 4.60.0 (Nov 27, 2024)
|
||||
|
||||
### Endpoint operations
|
||||
- Added support for labels_include_any to gitops.
|
||||
- Added major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation).
|
||||
- Added activity item for `fleetd` enrollment with host serial and display name.
|
||||
- Added capability for Fleet to serve YARA rules to agents over HTTPS authenticated via node key (requires osquery 5.14+).
|
||||
- Added a query to allow users to turn on/off automations while being transparent of the current log destination.
|
||||
- Updated UI to allow users to view scripts (from both the scripts page and host details page) without downloading them.
|
||||
- Updated activity feed to generate an activity when activity automations are enabled, edited, or disabled.
|
||||
- Cancelled pending script executions when a script is edited or deleted.
|
||||
|
||||
### Device management (MDM)
|
||||
- Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy.
|
||||
- Added info banner for cloud customers to help with their windows autoenrollment setup.
|
||||
- Added DB support for "include any" label profile deployment.
|
||||
- Added support for "include any" label/profile relationships to the profile reconciliation machinery.
|
||||
- Added `team_identifier` signature information to Apple macOS applications to the `/api/latest/fleet/hosts/:id/software` API endpoint.
|
||||
- Added indicator of how fresh a software title's host and version counts are on the title's details page.
|
||||
- Added UI for allowing users to install custom profiles on hosts that include any of the defined labels.
|
||||
- Added UI features supporting disk encryption for Ubuntu and Fedora Linux.
|
||||
- Added support for deb packages compressed with zstd.
|
||||
|
||||
### Vulnerability management
|
||||
- Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`).
|
||||
|
||||
### Bug fixes and improvements
|
||||
- Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required.
|
||||
- Improved performance of adding and removing profiles to large teams by an order of magnitude.
|
||||
- Disabled accessibility via keyboard for forms that are disabled via a slider.
|
||||
- Updated software batch endpoint status code from 200 (OK) to 202 (Accepted).
|
||||
- Updated a package used for testing (msw) to improve security.
|
||||
- Updated to reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04.
|
||||
- Updated GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there are more than 1 ABM tokens in Fleet.
|
||||
- Dismissed error flash on the my device page when navigating to another URL.
|
||||
- Modified the Fleet setup experience feature to not run if there is no software or script configured for the setup experience.
|
||||
- Set a more accurate minimum height for the Add hosts > ChromeOS > Policy for extension field, avoiding a scrollbar.
|
||||
- Added UI prompt for user to reenter the password if SCEP/NDES url or username has changed.
|
||||
- Updated ABM public key to download as as PEM format instead of CRT.
|
||||
- Fixed issue with uploading macOS software packages that do not have a top level `Distribution.xml`, but do have a top level `PackageInfo.xml`. For example, Okta Verify.app.
|
||||
- Fixed some cases where Fleet Maintained Apps generated incorrect uninstall scripts.
|
||||
- Fixed a bug where a device that was removed from ABM and then added back wouldn't properly re-enroll in Fleet MDM.
|
||||
- Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera.
|
||||
- Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica.
|
||||
- Fixed the MDM configuration profiles deployment when based on excluded labels.
|
||||
- Fixed gitops path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere).
|
||||
- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM IdP integration was enabled.
|
||||
- Fixed a bug where users would be allowed to attempt an install of an App Store app on a host that was not MDM enrolled.
|
||||
|
||||
## Fleet 4.59.1 (Nov 18, 2024)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* Added capability for Fleet to serve yara rules to agents over HTTPS authenticated via node key (requires osquery 5.14+).
|
||||
@@ -1 +0,0 @@
|
||||
* Improved memory usage of the Fleet server when uploading a large software installer file. Note that the installer will now use (temporary) disk space and sufficient storage space is required.
|
||||
@@ -1 +0,0 @@
|
||||
- Speed up adding and removing profiles to large teams by an order of magnitude
|
||||
@@ -1 +0,0 @@
|
||||
- add info banner for cloud customers to help with their windows autoenrollment setup
|
||||
@@ -1 +0,0 @@
|
||||
* Generate an activity when activity automations are enabled, edited, or disabled.
|
||||
@@ -1 +0,0 @@
|
||||
* Cancelled pending script executions when a script is edited or deleted.
|
||||
@@ -0,0 +1 @@
|
||||
* Replaced the internal use of the deprecated `go.mozilla.org/pkcs7` package with the maintained fork `github.com/smallstep/pkcs7`.
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed the MDM configuration profiles deployment when based on excluded labels - prior to this fix, hosts were considered "not a member" of the label by default, even if they had not yet returned results for the excluded labels. The fix checks the label's creation time vs the host's last reported label results timestamp to prevent deploying a configuration profile if it does not yet know if the host is a member or not of those labels.
|
||||
@@ -1 +0,0 @@
|
||||
* GitOps: Fixed path resolution for installer queries and scripts to always be relative to where the query file or script is referenced. This change breaks existing YAML files that had to account for previous inconsistent behavior (e.g. installers in a subdirectory referencing scripts elsewhere).
|
||||
@@ -1 +0,0 @@
|
||||
- Creating a query allow users to turn on/off automations while being transparent of the current log destination
|
||||
@@ -1 +0,0 @@
|
||||
* Added indicator of how fresh a software title's host and version counts are on the title's details page
|
||||
@@ -1,2 +0,0 @@
|
||||
- Updates GitOps to return an error if the deprecated `apple_bm_default_team` key is used and there
|
||||
are more than 1 ABM tokens in Fleet.
|
||||
@@ -1,2 +0,0 @@
|
||||
- Fixed issue where minimum OS version enforcement was not being applied during Apple ADE if MDM
|
||||
IdP integration was enabled.
|
||||
@@ -1 +0,0 @@
|
||||
- Reboot linux machine on unlock to work around GDM bug on Ubuntu 24.04
|
||||
@@ -1 +0,0 @@
|
||||
- Users can view scripts in the UI (from both the scripts page and host details page) without downloading them
|
||||
@@ -1,2 +0,0 @@
|
||||
- add UI for allowing users to install custom profiles on hosts that include any of the defined
|
||||
labels
|
||||
@@ -1 +0,0 @@
|
||||
- Add support for labels_include_any to gitops
|
||||
@@ -1 +0,0 @@
|
||||
- Adds DB support for "include any" label profile deployment
|
||||
@@ -1 +0,0 @@
|
||||
- Adds support for "include any" label/profile relationships to the profile reconciliation machinery.
|
||||
@@ -1 +0,0 @@
|
||||
- Fleet UI: Major improvements to keyboard accessibility throughout app (e.g. checkboxes, dropdowns, table navigation)
|
||||
@@ -1 +0,0 @@
|
||||
- Added UI features supporting disk encryption for Ubuntu and Fedora Linux.
|
||||
@@ -1 +0,0 @@
|
||||
- Fix some cases where Fleet Maintained Apps generated incorrect uninstall scripts
|
||||
@@ -1 +0,0 @@
|
||||
Added activity item for fleetd enrollment with host serial and display name.
|
||||
@@ -1 +0,0 @@
|
||||
- Add support for deb packages compressed with zstd
|
||||
@@ -1 +0,0 @@
|
||||
- Fleet UI: Disable accessibility via keyboard for forms that are disabled via a slider
|
||||
@@ -1,2 +0,0 @@
|
||||
* Set a more elegant minimum height for the Add hosts > ChromeOS > Policy for extension field,
|
||||
avoiding a scrollbar.
|
||||
@@ -1 +0,0 @@
|
||||
- Download ABM public key as PEM format instead of CRT
|
||||
@@ -1 +0,0 @@
|
||||
* Allowed skipping computationally heavy population of vulnerability details when populating host software on hosts list endpoint (`GET /api/latest/fleet/hosts`) when using Fleet Premium (`populate_software=without_vulnerability_descriptions`)
|
||||
@@ -1 +0,0 @@
|
||||
- update a package used for testing (msw) to improve security
|
||||
@@ -1,2 +0,0 @@
|
||||
- Fixes a bug where a device that was removed from ABM and then added back wouldn't properly
|
||||
re-enroll in Fleet MDM
|
||||
@@ -1 +0,0 @@
|
||||
Fixed issue with uploading macOS software packages that do not have a top level Distribution.xml, but do have a top level PackageInfo.xml. For example, Okta Verify.app
|
||||
@@ -1,2 +0,0 @@
|
||||
- Fixes a bug where users would be allowed to attempt an install of an App Store app on a host that
|
||||
was not MDM enrolled.
|
||||
@@ -0,0 +1 @@
|
||||
- Display Windows MDM WSTEP flags in `fleet --help`.
|
||||
@@ -1 +0,0 @@
|
||||
* Updated software batch endpoint status code from 200 (OK) to 202 (Accepted)
|
||||
@@ -1 +0,0 @@
|
||||
Added better handling of timeout and insufficient permissions errors in NDES SCEP proxy.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed name/version parsing issue with PE (EXE) installer self-extracting archives such as Opera.
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed a bug where the create and update label endpoints could return outdated information in a deployment using a mysql replica.
|
||||
@@ -1 +0,0 @@
|
||||
- Fleet UI: Prompt user to reenter the password if SCEP/NDES url or username has changed
|
||||
@@ -1 +0,0 @@
|
||||
* Dismiss error flash on the my device page when navigating to another URL.
|
||||
@@ -1,2 +0,0 @@
|
||||
- Modifies the Fleet setup experience feature to not run if there is no software or script
|
||||
configured for the setup experience.
|
||||
@@ -4,11 +4,11 @@ name: fleet
|
||||
keywords:
|
||||
- fleet
|
||||
- osquery
|
||||
version: v6.2.2
|
||||
version: v6.2.3
|
||||
home: https://github.com/fleetdm/fleet
|
||||
sources:
|
||||
- https://github.com/fleetdm/fleet.git
|
||||
appVersion: v4.59.1
|
||||
appVersion: v4.60.0
|
||||
dependencies:
|
||||
- name: mysql
|
||||
condition: mysql.enabled
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
hostName: fleet.localhost
|
||||
replicas: 3 # The number of Fleet instances to deploy
|
||||
imageRepository: fleetdm/fleet
|
||||
imageTag: v4.59.1 # Version of Fleet to deploy
|
||||
imageTag: v4.60.0 # Version of Fleet to deploy
|
||||
podAnnotations: {} # Additional annotations to add to the Fleet pod
|
||||
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
|
||||
resources:
|
||||
|
||||
@@ -37,7 +37,6 @@ export interface IModalProps {
|
||||
* */
|
||||
disableClosingModal?: boolean;
|
||||
className?: string;
|
||||
actionsFooter?: JSX.Element;
|
||||
}
|
||||
|
||||
const Modal = ({
|
||||
@@ -51,7 +50,6 @@ const Modal = ({
|
||||
isContentDisabled = false,
|
||||
disableClosingModal = false,
|
||||
className,
|
||||
actionsFooter,
|
||||
}: IModalProps): JSX.Element => {
|
||||
useEffect(() => {
|
||||
const closeWithEscapeKey = (e: KeyboardEvent) => {
|
||||
@@ -127,9 +125,6 @@ const Modal = ({
|
||||
)}
|
||||
<div className={contentClasses}>{children}</div>
|
||||
</div>
|
||||
{actionsFooter && (
|
||||
<div className={`${baseClass}__actions-footer`}>{actionsFooter}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -25,17 +25,9 @@
|
||||
&__content-wrapper {
|
||||
margin-top: $pad-large;
|
||||
font-size: $x-small;
|
||||
// New pattern of max height modals pushed to 4.61 with PR #24019
|
||||
overflow: visible;
|
||||
|
||||
.input-field {
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
font-size: $x-small;
|
||||
font-style: italic;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
form .modal-cta-wrap,
|
||||
@@ -142,6 +134,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-scrollable-content {
|
||||
overflow-y: auto;
|
||||
max-height: 705px;
|
||||
}
|
||||
|
||||
.modal-cta-wrap {
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable no-alert */
|
||||
import React from "react";
|
||||
import { Meta, Story } from "@storybook/react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
import ActionsDropdown from "components/ActionsDropdown";
|
||||
import ModalFooter from "./ModalFooter";
|
||||
|
||||
export default {
|
||||
title: "Components/ModalFooter",
|
||||
component: ModalFooter,
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<ModalFooter primaryButtons={<></>} {...args} />
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
primaryButtons: (
|
||||
<>
|
||||
<ActionsDropdown
|
||||
className="modal-footer__manage-automations-dropdown"
|
||||
onChange={(value) => alert(`Selected action: ${value}`)}
|
||||
placeholder="More actions"
|
||||
isSearchable={false}
|
||||
options={[
|
||||
{ value: "action1", label: "Action 1" },
|
||||
{ value: "action2", label: "Action 2" },
|
||||
]}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
<Button onClick={() => alert("Done clicked")} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
secondaryButtons: (
|
||||
<>
|
||||
<Button variant="icon" onClick={() => alert("Download clicked")}>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button variant="icon" onClick={() => alert("Delete clicked")}>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
isTopScrolling: false,
|
||||
};
|
||||
|
||||
export const WithTopScrolling = Template.bind({});
|
||||
WithTopScrolling.args = {
|
||||
...Default.args,
|
||||
isTopScrolling: true,
|
||||
};
|
||||
|
||||
export const WithoutSecondaryButtons = Template.bind({});
|
||||
WithoutSecondaryButtons.args = {
|
||||
primaryButtons: Default.args.primaryButtons,
|
||||
secondaryButtons: undefined,
|
||||
isTopScrolling: false,
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
const baseClass = "modal-footer";
|
||||
|
||||
interface IModalFooterProps {
|
||||
primaryButtons: JSX.Element;
|
||||
secondaryButtons?: JSX.Element;
|
||||
className?: string;
|
||||
/** Renders a line above action buttons to indicate scrollability */
|
||||
isTopScrolling?: boolean;
|
||||
}
|
||||
|
||||
const ModalFooter = ({
|
||||
primaryButtons,
|
||||
secondaryButtons,
|
||||
className,
|
||||
isTopScrolling = false,
|
||||
}: IModalFooterProps): JSX.Element => {
|
||||
const classes = classnames(className, `${baseClass}__content-wrapper`);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes}
|
||||
style={{
|
||||
borderTop: isTopScrolling
|
||||
? `1px solid ${COLORS["ui-fleet-black-50"]}`
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
<div className={`${baseClass}__primary-buttons-wrapper`}>
|
||||
{primaryButtons}
|
||||
</div>
|
||||
{secondaryButtons && (
|
||||
<div className={`${baseClass}__secondary-buttons-wrapper`}>
|
||||
{secondaryButtons}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalFooter;
|
||||
@@ -0,0 +1,27 @@
|
||||
.modal-footer {
|
||||
&__content-wrapper {
|
||||
align-self: flex-end;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
padding-top: $pad-medium;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// Styles both primary-actions and secondary-actions
|
||||
&__primary-buttons-wrapper,
|
||||
&__secondary-buttons_wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: $pad-medium;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Align primary actions right if no secondary actions
|
||||
> :last-child {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.button__text-icon {
|
||||
padding: 11px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ModalFooter";
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
|
||||
import Icon from "components/Icon";
|
||||
|
||||
interface INumberDropdownOption extends Omit<IDropdownOption, "value"> {
|
||||
export interface INumberDropdownOption extends Omit<IDropdownOption, "value"> {
|
||||
value: number; // Redefine the value property to be just number
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Used with old react-select dropdown and
|
||||
// New react-select-5 ActionsDropdown.tsx
|
||||
// New react-select-5: ActionsDropdown.tsx, DropdownWrapper.tsx
|
||||
.Select > .Select-menu-outer,
|
||||
.actions-dropdown {
|
||||
.actions-dropdown,
|
||||
.react-select__option {
|
||||
.is-disabled * {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// stories/DropdownWrapper.stories.tsx
|
||||
|
||||
import React from "react";
|
||||
import { Meta, Story } from "@storybook/react";
|
||||
import DropdownWrapper, {
|
||||
IDropdownWrapper,
|
||||
CustomOptionType,
|
||||
} from "./DropdownWrapper";
|
||||
|
||||
// Define metadata for the story
|
||||
export default {
|
||||
title: "Components/DropdownWrapper",
|
||||
component: DropdownWrapper,
|
||||
argTypes: {
|
||||
onChange: { action: "changed" },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
// Define a template for the stories
|
||||
const Template: Story<IDropdownWrapper> = (args) => (
|
||||
<DropdownWrapper {...args} />
|
||||
);
|
||||
|
||||
// Sample options to be used in the dropdown
|
||||
const sampleOptions: CustomOptionType[] = [
|
||||
{ label: "Option 1", value: "option1", helpText: "Help text for option 1" },
|
||||
{
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
tooltipContent: "Tooltip for option 2",
|
||||
},
|
||||
{ label: "Option 3", value: "option3", isDisabled: true },
|
||||
];
|
||||
|
||||
// Default story
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
options: sampleOptions,
|
||||
name: "dropdown-example",
|
||||
label: "Select an option",
|
||||
};
|
||||
|
||||
// Disabled story
|
||||
export const Disabled = Template.bind({});
|
||||
Disabled.args = {
|
||||
...Default.args,
|
||||
isDisabled: true,
|
||||
};
|
||||
|
||||
// With Help Text story
|
||||
export const WithHelpText = Template.bind({});
|
||||
WithHelpText.args = {
|
||||
...Default.args,
|
||||
helpText: "This is some help text for the dropdown",
|
||||
};
|
||||
|
||||
// With Error story
|
||||
export const WithError = Template.bind({});
|
||||
WithError.args = {
|
||||
...Default.args,
|
||||
error: "This is an error message",
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import DropdownWrapper, { CustomOptionType } from "./DropdownWrapper";
|
||||
|
||||
const sampleOptions: CustomOptionType[] = [
|
||||
{
|
||||
label: "Option 1",
|
||||
value: "option1",
|
||||
tooltipContent: "Tooltip 1",
|
||||
helpText: "Help text 1",
|
||||
},
|
||||
{
|
||||
label: "Option 2",
|
||||
value: "option2",
|
||||
tooltipContent: "Tooltip 2",
|
||||
helpText: "Help text 2",
|
||||
},
|
||||
];
|
||||
|
||||
describe("DropdownWrapper Component", () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("renders with help text", () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
helpText="This is a help text."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/test dropdown/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/this is a help text/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("calls onChange when an option is selected", async () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
placeholder="Choose option"
|
||||
/>
|
||||
);
|
||||
|
||||
// Open the dropdown
|
||||
await userEvent.click(screen.getByText(/option 1/i));
|
||||
|
||||
// Select Option 2
|
||||
await userEvent.click(screen.getByText(/option 2/i));
|
||||
|
||||
expect(mockOnChange).toHaveBeenCalledWith({
|
||||
helpText: "Help text 2",
|
||||
label: "Option 2",
|
||||
tooltipContent: "Tooltip 2",
|
||||
value: "option2",
|
||||
});
|
||||
});
|
||||
|
||||
test("renders error message when provided", () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={sampleOptions}
|
||||
value="option1"
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
error="This is an error message."
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/this is an error message/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("displays no options message when no options are available", async () => {
|
||||
render(
|
||||
<DropdownWrapper
|
||||
options={[]}
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
name="test-dropdown"
|
||||
label="Test Dropdown"
|
||||
placeholder="Choose option"
|
||||
/>
|
||||
);
|
||||
|
||||
// Open dropdown
|
||||
await userEvent.click(screen.getByText(/choose option/i));
|
||||
|
||||
expect(screen.getByText(/no results found/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
/**
|
||||
* This is a new component built off react-select 5.4
|
||||
* meant to replace Dropdown.jsx built off react-select 1.3
|
||||
*
|
||||
* See storybook component for current functionality
|
||||
*
|
||||
* Prototyped on UserForm.tsx but added and tested the following:
|
||||
* Options: text, disabled, option helptext, option tooltip
|
||||
* Other: label text, dropdown help text, dropdown error
|
||||
*/
|
||||
|
||||
import classnames from "classnames";
|
||||
import React from "react";
|
||||
import Select, {
|
||||
StylesConfig,
|
||||
DropdownIndicatorProps,
|
||||
OptionProps,
|
||||
components,
|
||||
PropsValue,
|
||||
SingleValue,
|
||||
} from "react-select-5";
|
||||
|
||||
import { COLORS } from "styles/var/colors";
|
||||
import { PADDING } from "styles/var/padding";
|
||||
|
||||
import FormField from "components/forms/FormField";
|
||||
import DropdownOptionTooltipWrapper from "components/forms/fields/Dropdown/DropdownOptionTooltipWrapper";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
const getOptionBackgroundColor = (state: any) => {
|
||||
return state.isSelected || state.isFocused
|
||||
? COLORS["ui-vibrant-blue-10"]
|
||||
: "transparent";
|
||||
};
|
||||
|
||||
export interface CustomOptionType {
|
||||
label: string;
|
||||
value: string;
|
||||
tooltipContent?: string;
|
||||
helpText?: string;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface IDropdownWrapper {
|
||||
options: CustomOptionType[];
|
||||
value?: PropsValue<CustomOptionType> | string;
|
||||
onChange: (newValue: SingleValue<CustomOptionType>) => void;
|
||||
name: string;
|
||||
className?: string;
|
||||
labelClassname?: string;
|
||||
error?: string;
|
||||
label?: JSX.Element | string;
|
||||
helpText?: JSX.Element | string;
|
||||
isSearchable?: boolean;
|
||||
isDisabled?: boolean;
|
||||
placeholder?: string;
|
||||
menuPortalTarget?: HTMLElement | null;
|
||||
}
|
||||
|
||||
const baseClass = "dropdown-wrapper";
|
||||
|
||||
const DropdownWrapper = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
className,
|
||||
labelClassname,
|
||||
error,
|
||||
label,
|
||||
helpText,
|
||||
isSearchable,
|
||||
isDisabled = false,
|
||||
placeholder,
|
||||
menuPortalTarget,
|
||||
}: IDropdownWrapper) => {
|
||||
const wrapperClassNames = classnames(baseClass, className);
|
||||
|
||||
const handleChange = (newValue: SingleValue<CustomOptionType>) => {
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
// Ability to handle value of type string or CustomOptionType
|
||||
const getCurrentValue = () => {
|
||||
if (typeof value === "string") {
|
||||
return options.find((option) => option.value === value) || null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
interface CustomOptionProps
|
||||
extends Omit<OptionProps<CustomOptionType, false>, "data"> {
|
||||
data: CustomOptionType;
|
||||
}
|
||||
|
||||
const CustomOption = (props: CustomOptionProps) => {
|
||||
const { data, ...rest } = props;
|
||||
|
||||
const optionContent = (
|
||||
<div className={`${baseClass}__option`}>
|
||||
{data.label}
|
||||
{data.helpText && (
|
||||
<span className={`${baseClass}__help-text`}>{data.helpText}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<components.Option {...rest} data={data}>
|
||||
{data.tooltipContent ? (
|
||||
<DropdownOptionTooltipWrapper tipContent={data.tooltipContent}>
|
||||
{optionContent}
|
||||
</DropdownOptionTooltipWrapper>
|
||||
) : (
|
||||
optionContent
|
||||
)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomDropdownIndicator = (
|
||||
props: DropdownIndicatorProps<any, false, any>
|
||||
) => {
|
||||
const { isFocused, selectProps } = props;
|
||||
const color =
|
||||
isFocused || selectProps.menuIsOpen
|
||||
? "core-fleet-blue"
|
||||
: "core-fleet-black";
|
||||
|
||||
return (
|
||||
<components.DropdownIndicator
|
||||
{...props}
|
||||
className={`${baseClass}__indicator`}
|
||||
>
|
||||
<Icon
|
||||
name="chevron-down"
|
||||
color={color}
|
||||
className={`${baseClass}__icon`}
|
||||
/>
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
};
|
||||
|
||||
const customStyles: StylesConfig<CustomOptionType, false> = {
|
||||
container: (provided) => ({
|
||||
...provided,
|
||||
width: "100%",
|
||||
height: "40px",
|
||||
}),
|
||||
control: (provided, state) => ({
|
||||
...provided,
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
backgroundColor: COLORS["ui-off-white"],
|
||||
paddingLeft: "8px", // TODO: Update to match styleguide of (16px) when updating rest of UI (8px)
|
||||
paddingRight: "8px",
|
||||
cursor: "pointer",
|
||||
boxShadow: "none",
|
||||
borderRadius: "4px",
|
||||
borderColor: state.isFocused
|
||||
? COLORS["core-fleet-blue"]
|
||||
: COLORS["ui-fleet-black-10"],
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
borderColor: COLORS["core-fleet-blue"],
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
},
|
||||
// When tabbing
|
||||
// Relies on --is-focused for styling as &:focus-visible cannot be applied
|
||||
"&.dropdown-wrapper__control--is-focused": {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-over"],
|
||||
},
|
||||
},
|
||||
...(state.isDisabled && {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["ui-fleet-black-50"],
|
||||
},
|
||||
}),
|
||||
"&:active": {
|
||||
".dropdown-wrapper__single-value": {
|
||||
color: COLORS["core-vibrant-blue-down"],
|
||||
},
|
||||
".dropdown-wrapper__indicator path": {
|
||||
stroke: COLORS["core-vibrant-blue-down"],
|
||||
},
|
||||
},
|
||||
...(state.menuIsOpen && {
|
||||
".dropdown-wrapper__indicator svg": {
|
||||
transform: "rotate(180deg)",
|
||||
transition: "transform 0.25s ease",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
singleValue: (provided) => ({
|
||||
...provided,
|
||||
fontSize: "16px",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}),
|
||||
dropdownIndicator: (provided) => ({
|
||||
...provided,
|
||||
display: "flex",
|
||||
padding: "2px",
|
||||
svg: {
|
||||
transition: "transform 0.25s ease",
|
||||
},
|
||||
}),
|
||||
menu: (provided) => ({
|
||||
...provided,
|
||||
boxShadow: "0 2px 6px rgba(0, 0, 0, 0.1)",
|
||||
borderRadius: "4px",
|
||||
zIndex: 6,
|
||||
overflow: "hidden",
|
||||
border: 0,
|
||||
marginTop: 0,
|
||||
maxHeight: "none",
|
||||
position: "absolute",
|
||||
left: "0",
|
||||
animation: "fade-in 150ms ease-out",
|
||||
}),
|
||||
menuList: (provided) => ({
|
||||
...provided,
|
||||
padding: PADDING["pad-small"],
|
||||
}),
|
||||
valueContainer: (provided) => ({
|
||||
...provided,
|
||||
padding: 0,
|
||||
}),
|
||||
option: (provided, state) => ({
|
||||
...provided,
|
||||
padding: "10px 8px",
|
||||
fontSize: "14px",
|
||||
backgroundColor: getOptionBackgroundColor(state),
|
||||
color: COLORS["core-fleet-black"],
|
||||
"&:hover": {
|
||||
backgroundColor: state.isDisabled
|
||||
? "transparent"
|
||||
: COLORS["ui-vibrant-blue-10"],
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: state.isDisabled
|
||||
? "transparent"
|
||||
: COLORS["ui-vibrant-blue-10"],
|
||||
},
|
||||
...(state.isDisabled && {
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
fontStyle: "italic",
|
||||
cursor: "not-allowed",
|
||||
pointerEvents: "none",
|
||||
}),
|
||||
// Styles for custom option
|
||||
".dropdown-wrapper__option": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
width: "100%",
|
||||
},
|
||||
".dropdown-wrapper__help-text": {
|
||||
fontSize: "12px",
|
||||
whiteSpace: "normal",
|
||||
color: COLORS["ui-fleet-black-50"],
|
||||
fontStyle: "italic",
|
||||
},
|
||||
}),
|
||||
menuPortal: (base) => ({ ...base, zIndex: 999 }), // Not hidden beneath scrollable sections
|
||||
noOptionsMessage: (provided) => ({
|
||||
...provided,
|
||||
textAlign: "left",
|
||||
fontSize: "14px",
|
||||
padding: "10px 8px",
|
||||
}),
|
||||
};
|
||||
|
||||
const renderLabel = () => {
|
||||
const labelWrapperClasses = classnames(
|
||||
`${baseClass}__label`,
|
||||
labelClassname,
|
||||
{
|
||||
[`${baseClass}__label--error`]: !!error,
|
||||
[`${baseClass}__label--disabled`]: isDisabled,
|
||||
}
|
||||
);
|
||||
|
||||
if (!label) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return (
|
||||
<label className={labelWrapperClasses} htmlFor={name}>
|
||||
{error || label}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField
|
||||
name={name}
|
||||
label={renderLabel()}
|
||||
helpText={helpText}
|
||||
type="dropdown"
|
||||
className={wrapperClassNames}
|
||||
>
|
||||
<Select<CustomOptionType, false>
|
||||
classNamePrefix="react-select"
|
||||
isSearchable={isSearchable}
|
||||
styles={customStyles}
|
||||
options={options}
|
||||
components={{
|
||||
Option: CustomOption,
|
||||
DropdownIndicator: CustomDropdownIndicator,
|
||||
IndicatorSeparator: () => null,
|
||||
}}
|
||||
value={getCurrentValue()}
|
||||
onChange={handleChange}
|
||||
isDisabled={isDisabled}
|
||||
menuPortalTarget={
|
||||
menuPortalTarget === undefined ? document.body : menuPortalTarget
|
||||
}
|
||||
noOptionsMessage={() => "No results found"}
|
||||
tabIndex={isDisabled ? -1 : 0} // Ensures disabled dropdown has no keyboard accessibility
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
|
||||
export default DropdownWrapper;
|
||||
@@ -0,0 +1,14 @@
|
||||
// react-select's <Select/> styles prop customizes the styling of
|
||||
// the internal components and not external elements like labels
|
||||
// See customStyles in DropdownWrappr.tsx
|
||||
// https://react-select.com/styles
|
||||
.dropdown-wrapper {
|
||||
&__label {
|
||||
&--error {
|
||||
color: $ui-error;
|
||||
}
|
||||
&--disabled {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DropdownWrapper";
|
||||
@@ -1,16 +0,0 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { UserRole } from "./user";
|
||||
|
||||
export default PropTypes.shape({
|
||||
disabled: PropTypes.bool,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
helpText: PropTypes.string,
|
||||
});
|
||||
|
||||
export interface IRole {
|
||||
disabled: boolean;
|
||||
label: string;
|
||||
value: UserRole;
|
||||
helpText?: string;
|
||||
}
|
||||
+38
-11
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import scriptsAPI, { IScriptResultResponse } from "services/entities/scripts";
|
||||
@@ -10,6 +10,7 @@ import Icon from "components/Icon";
|
||||
import Textarea from "components/Textarea";
|
||||
import DataError from "components/DataError/DataError";
|
||||
import Spinner from "components/Spinner/Spinner";
|
||||
import ModalFooter from "components/ModalFooter";
|
||||
|
||||
const baseClass = "run-script-details-modal";
|
||||
|
||||
@@ -166,6 +167,17 @@ const RunScriptDetailsModal = ({
|
||||
onCancel,
|
||||
isHidden = false,
|
||||
}: IRunScriptDetailsModalProps) => {
|
||||
// For scrollable modal
|
||||
const [isTopScrolling, setIsTopScrolling] = useState(false);
|
||||
const topDivRef = useRef<HTMLDivElement>(null);
|
||||
const checkScroll = () => {
|
||||
if (topDivRef.current) {
|
||||
const isScrolling =
|
||||
topDivRef.current.scrollHeight > topDivRef.current.clientHeight;
|
||||
setIsTopScrolling(isScrolling);
|
||||
}
|
||||
};
|
||||
|
||||
const { data, isLoading, isError } = useQuery<IScriptResultResponse>(
|
||||
["runScriptDetailsModal", scriptExecutionId],
|
||||
() => {
|
||||
@@ -174,6 +186,13 @@ const RunScriptDetailsModal = ({
|
||||
{ refetchOnWindowFocus: false, enabled: !!scriptExecutionId }
|
||||
);
|
||||
|
||||
// For scrollable modal
|
||||
useEffect(() => {
|
||||
checkScroll();
|
||||
window.addEventListener("resize", checkScroll);
|
||||
return () => window.removeEventListener("resize", checkScroll);
|
||||
}, [data]); // Re-run when data changes
|
||||
|
||||
const renderContent = () => {
|
||||
let content = <></>;
|
||||
|
||||
@@ -206,18 +225,24 @@ const RunScriptDetailsModal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${baseClass}__modal-content`}>{content}</div>
|
||||
</>
|
||||
<div
|
||||
className={`${baseClass}__modal-content modal-scrollable-content`}
|
||||
ref={topDivRef}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderFooter = () => (
|
||||
<div className={`primary-actions ${baseClass}__host-script-actions`}>
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
<ModalFooter
|
||||
isTopScrolling={isTopScrolling}
|
||||
primaryButtons={
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
@@ -226,9 +251,11 @@ const RunScriptDetailsModal = ({
|
||||
onEnter={onCancel}
|
||||
className={baseClass}
|
||||
isHidden={isHidden}
|
||||
actionsFooter={renderFooter()}
|
||||
>
|
||||
{renderContent()}
|
||||
<>
|
||||
{renderContent()}
|
||||
{renderFooter()}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+86
-51
@@ -1,4 +1,10 @@
|
||||
import React, { useCallback, useContext, ReactNode } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
useQuery,
|
||||
@@ -15,6 +21,7 @@ import { IHostScript } from "interfaces/script";
|
||||
import { IApiError, getErrorReason } from "interfaces/errors";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import ModalFooter from "components/ModalFooter";
|
||||
import Button from "components/buttons/Button";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon";
|
||||
@@ -68,6 +75,17 @@ const ScriptDetailsModal = ({
|
||||
isHidden = false,
|
||||
onClickRunDetails,
|
||||
}: IScriptDetailsModalProps) => {
|
||||
// For scrollable modal
|
||||
const [isTopScrolling, setIsTopScrolling] = useState(false);
|
||||
const topDivRef = useRef<HTMLDivElement>(null);
|
||||
const checkScroll = () => {
|
||||
if (topDivRef.current) {
|
||||
const isScrolling =
|
||||
topDivRef.current.scrollHeight > topDivRef.current.clientHeight;
|
||||
setIsTopScrolling(isScrolling);
|
||||
}
|
||||
};
|
||||
|
||||
const { currentUser } = useContext(AppContext);
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
@@ -87,6 +105,14 @@ const ScriptDetailsModal = ({
|
||||
enabled: !selectedScriptContent && !!selectedScriptDetails?.script_id,
|
||||
}
|
||||
);
|
||||
|
||||
// For scrollable modal
|
||||
useEffect(() => {
|
||||
checkScroll();
|
||||
window.addEventListener("resize", checkScroll);
|
||||
return () => window.removeEventListener("resize", checkScroll);
|
||||
}, [scriptContent]); // Re-run when data changes
|
||||
|
||||
const getScriptContent = async () => {
|
||||
try {
|
||||
const content = selectedScriptContent || scriptContent;
|
||||
@@ -157,60 +183,64 @@ const ScriptDetailsModal = ({
|
||||
]
|
||||
);
|
||||
|
||||
const shouldShowFooter = () => {
|
||||
return !isLoadingScriptContent && selectedScriptDetails !== undefined;
|
||||
};
|
||||
const shouldShowFooter =
|
||||
!isLoadingScriptContent && selectedScriptDetails !== undefined;
|
||||
|
||||
const renderFooter = () => {
|
||||
if (!shouldShowFooter) {
|
||||
return <></>;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`secondary-actions ${baseClass}__script-actions`}>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={() => onClickDownload()}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className={`primary-actions ${baseClass}__host-script-actions`}>
|
||||
{showHostScriptActions && selectedScriptDetails && (
|
||||
<div className={`${baseClass}__manage-automations-wrapper`}>
|
||||
<ActionsDropdown
|
||||
className={`${baseClass}__manage-automations-dropdown`}
|
||||
onChange={(value) =>
|
||||
onSelectMoreActions(
|
||||
value,
|
||||
<ModalFooter
|
||||
isTopScrolling={isTopScrolling}
|
||||
secondaryButtons={
|
||||
<>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={() => onClickDownload()}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="icon"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
primaryButtons={
|
||||
<>
|
||||
{showHostScriptActions && selectedScriptDetails && (
|
||||
<div className={`${baseClass}__manage-automations-wrapper`}>
|
||||
<ActionsDropdown
|
||||
className={`${baseClass}__manage-automations-dropdown`}
|
||||
onChange={(value) =>
|
||||
onSelectMoreActions(
|
||||
value,
|
||||
selectedScriptDetails as IHostScript
|
||||
)
|
||||
}
|
||||
placeholder="More actions"
|
||||
isSearchable={false}
|
||||
options={generateActionDropdownOptions(
|
||||
currentUser,
|
||||
hostTeamId || null,
|
||||
selectedScriptDetails as IHostScript
|
||||
)
|
||||
}
|
||||
placeholder="More actions"
|
||||
isSearchable={false}
|
||||
options={generateActionDropdownOptions(
|
||||
currentUser,
|
||||
hostTeamId || null,
|
||||
selectedScriptDetails as IHostScript
|
||||
)}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
menuPlacement="top"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -224,7 +254,10 @@ const ScriptDetailsModal = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__script-content`}>
|
||||
<div
|
||||
className={`${baseClass}__script-content modal-scrollable-content`}
|
||||
ref={topDivRef}
|
||||
>
|
||||
<span>Script content:</span>
|
||||
<Textarea className={`${baseClass}__script-content-textarea`}>
|
||||
{scriptContent}
|
||||
@@ -249,10 +282,12 @@ const ScriptDetailsModal = ({
|
||||
title={selectedScriptDetails?.name || "Script details"}
|
||||
width="large"
|
||||
onExit={onCancel}
|
||||
actionsFooter={shouldShowFooter() ? renderFooter() : undefined}
|
||||
isHidden={isHidden}
|
||||
>
|
||||
{renderContent()}
|
||||
<>
|
||||
{renderContent()}
|
||||
{shouldShowFooter ? renderFooter() : undefined}
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
|
||||
&__script-content-textarea {
|
||||
font-family: "SourceCodePro";
|
||||
min-height: 150px;
|
||||
max-height: 572px;
|
||||
overflow: auto;
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import React, { useCallback, useContext, useMemo, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { Link } from "react-router";
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from "react";
|
||||
import { InjectedRouter } from "react-router";
|
||||
import SandboxGate from "components/Sandbox/SandboxGate";
|
||||
import SandboxMessage from "components/Sandbox/SandboxMessage";
|
||||
import UsersTable from "./components/UsersTable";
|
||||
|
||||
const baseClass = "user-management";
|
||||
@@ -17,17 +15,7 @@ const UserManagementPage = ({ router }: IUserManagementProps): JSX.Element => {
|
||||
Create new users, customize user permissions, and remove users from
|
||||
Fleet.
|
||||
</p>
|
||||
<SandboxGate
|
||||
fallbackComponent={() => (
|
||||
<SandboxMessage
|
||||
message="User management is only available in self-managed Fleet"
|
||||
utmSource="fleet-ui-users-page"
|
||||
className={`${baseClass}__sandbox-message`}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<UsersTable router={router} />
|
||||
</SandboxGate>
|
||||
<UsersTable router={router} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+27
-38
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { UserRole } from "interfaces/user";
|
||||
// ignore TS error for now until these are rewritten in ts.
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { SingleValue } from "react-select-5";
|
||||
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { AppContext } from "context/app";
|
||||
import { roleOptions } from "../../helpers/userManagementHelpers";
|
||||
|
||||
@@ -13,29 +12,19 @@ interface ISelectRoleFormProps {
|
||||
currentTeam?: ITeam;
|
||||
teams: ITeam[];
|
||||
onFormChange: (teams: ITeam[]) => void;
|
||||
label: string | string[];
|
||||
isApiOnly?: boolean;
|
||||
}
|
||||
|
||||
const generateSelectedTeamData = (
|
||||
allTeams: ITeam[],
|
||||
updatedTeam?: any
|
||||
updatedTeam?: Partial<ITeam>
|
||||
): ITeam[] => {
|
||||
const filtered = allTeams.map(
|
||||
(teamItem): ITeam => {
|
||||
const teamRole =
|
||||
teamItem.id === updatedTeam?.id ? updatedTeam.role : teamItem.role;
|
||||
return {
|
||||
description: teamItem.description,
|
||||
id: teamItem.id,
|
||||
host_count: teamItem.host_count,
|
||||
user_count: teamItem.user_count,
|
||||
name: teamItem.name,
|
||||
role: teamRole,
|
||||
};
|
||||
}
|
||||
return allTeams.map(
|
||||
(teamItem): ITeam => ({
|
||||
...teamItem,
|
||||
role: teamItem.id === updatedTeam?.id ? updatedTeam.role! : teamItem.role,
|
||||
})
|
||||
);
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const SelectRoleForm = ({
|
||||
@@ -43,33 +32,33 @@ const SelectRoleForm = ({
|
||||
currentTeam,
|
||||
teams,
|
||||
onFormChange,
|
||||
label,
|
||||
isApiOnly,
|
||||
}: ISelectRoleFormProps): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
|
||||
const [selectedRole, setSelectedRole] = useState(
|
||||
defaultTeamRole.toLowerCase()
|
||||
);
|
||||
const [selectedRole, setSelectedRole] = useState<CustomOptionType>({
|
||||
value: defaultTeamRole.toLowerCase(),
|
||||
label: defaultTeamRole,
|
||||
});
|
||||
|
||||
const updateSelectedRole = (newRoleValue: UserRole) => {
|
||||
const updatedTeam = { ...currentTeam };
|
||||
|
||||
updatedTeam.role = newRoleValue;
|
||||
|
||||
onFormChange(generateSelectedTeamData(teams, updatedTeam));
|
||||
|
||||
setSelectedRole(newRoleValue);
|
||||
const updateSelectedRole = (newRoleValue: SingleValue<CustomOptionType>) => {
|
||||
if (newRoleValue) {
|
||||
const updatedTeam = {
|
||||
...currentTeam,
|
||||
role: newRoleValue.value as UserRole,
|
||||
};
|
||||
onFormChange(generateSelectedTeamData(teams, updatedTeam));
|
||||
setSelectedRole(newRoleValue);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
label={label}
|
||||
value={selectedRole}
|
||||
<DropdownWrapper
|
||||
name="Team role"
|
||||
options={roleOptions({ isPremiumTier, isApiOnly })}
|
||||
searchable={false}
|
||||
onChange={(newRoleValue: UserRole) => updateSelectedRole(newRoleValue)}
|
||||
testId={`${name}-checkbox`}
|
||||
value={selectedRole}
|
||||
onChange={updateSelectedRole}
|
||||
isSearchable={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+11
-10
@@ -3,8 +3,9 @@ import React, { useState } from "react";
|
||||
import { ITeam } from "interfaces/team";
|
||||
import { UserRole } from "interfaces/user";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
// @ts-ignore
|
||||
import Dropdown from "components/forms/fields/Dropdown";
|
||||
import { SingleValue } from "react-select-5";
|
||||
import DropdownWrapper from "components/forms/fields/DropdownWrapper";
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { roleOptions } from "../../helpers/userManagementHelpers";
|
||||
|
||||
interface ITeamCheckboxListItem extends ITeam {
|
||||
@@ -62,7 +63,7 @@ const generateSelectedTeamData = (
|
||||
const updateFormState = (
|
||||
prevTeamItems: ITeamCheckboxListItem[],
|
||||
teamId: number,
|
||||
newValue: UserRole | boolean | undefined
|
||||
newValue: SingleValue<CustomOptionType> | boolean | undefined
|
||||
): ITeamCheckboxListItem[] => {
|
||||
const prevItemIndex = prevTeamItems.findIndex((item) => item.id === teamId);
|
||||
const prevItem = prevTeamItems[prevItemIndex];
|
||||
@@ -70,7 +71,7 @@ const updateFormState = (
|
||||
if (typeof newValue === "boolean") {
|
||||
prevItem.isChecked = newValue;
|
||||
} else {
|
||||
prevItem.role = newValue;
|
||||
prevItem.role = newValue?.value as UserRole;
|
||||
}
|
||||
|
||||
return [...prevTeamItems];
|
||||
@@ -87,7 +88,7 @@ const useSelectedTeamState = (
|
||||
|
||||
const updateSelectedTeams = (
|
||||
teamId: number,
|
||||
newValue: UserRole | boolean
|
||||
newValue: CustomOptionType | boolean
|
||||
) => {
|
||||
setTeamsFormList((prevState) => {
|
||||
const updatedTeamFormList = updateFormState(prevState, teamId, newValue);
|
||||
@@ -127,15 +128,15 @@ const SelectedTeamsForm = ({
|
||||
>
|
||||
{name}
|
||||
</Checkbox>
|
||||
<Dropdown
|
||||
<DropdownWrapper
|
||||
name={name}
|
||||
value={role}
|
||||
className={`${baseClass}__role-dropdown`}
|
||||
options={roleOptions({ isPremiumTier: true, isApiOnly })}
|
||||
searchable={false}
|
||||
onChange={(newValue: UserRole) =>
|
||||
updateSelectedTeams(teamItem.id, newValue)
|
||||
isSearchable={false}
|
||||
onChange={(newValue: SingleValue<CustomOptionType>) =>
|
||||
updateSelectedTeams(teamItem.id, newValue as CustomOptionType)
|
||||
}
|
||||
testId={`${name}-checkbox`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
justify-content: space-between;
|
||||
|
||||
.form-field--dropdown {
|
||||
width: auto;
|
||||
width: 154px; // Matches dropdown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -370,7 +370,6 @@ const UserForm = ({
|
||||
</>
|
||||
) : (
|
||||
<SelectRoleForm
|
||||
label="Role"
|
||||
currentTeam={currentTeam || formData.teams[0]}
|
||||
teams={formData.teams}
|
||||
defaultTeamRole={defaultTeamRole || "observer"}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { CustomOptionType } from "components/forms/fields/DropdownWrapper/DropdownWrapper";
|
||||
import { IInvite } from "interfaces/invite";
|
||||
import { IUser, IUserUpdateBody, IUpdateUserFormData } from "interfaces/user";
|
||||
import { IRole } from "interfaces/role";
|
||||
import { IFormData } from "../components/UserForm/UserForm";
|
||||
|
||||
type ICurrentUserData = Pick<
|
||||
@@ -10,7 +10,7 @@ type ICurrentUserData = Pick<
|
||||
"global_role" | "teams" | "name" | "email" | "sso_enabled"
|
||||
>;
|
||||
|
||||
interface IRoleOptionsParams {
|
||||
export interface IRoleOptionsParams {
|
||||
isPremiumTier?: boolean;
|
||||
isApiOnly?: boolean;
|
||||
}
|
||||
@@ -58,20 +58,17 @@ const generateUpdateData = (
|
||||
export const roleOptions = ({
|
||||
isPremiumTier,
|
||||
isApiOnly,
|
||||
}: IRoleOptionsParams): IRole[] => {
|
||||
const roles: IRole[] = [
|
||||
}: IRoleOptionsParams): CustomOptionType[] => {
|
||||
const roles: CustomOptionType[] = [
|
||||
{
|
||||
disabled: false,
|
||||
label: "Observer",
|
||||
value: "observer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Maintainer",
|
||||
value: "maintainer",
|
||||
},
|
||||
{
|
||||
disabled: false,
|
||||
label: "Admin",
|
||||
value: "admin",
|
||||
},
|
||||
@@ -79,14 +76,12 @@ export const roleOptions = ({
|
||||
|
||||
if (isPremiumTier) {
|
||||
roles.splice(1, 0, {
|
||||
disabled: false,
|
||||
label: "Observer+",
|
||||
value: "observer_plus",
|
||||
});
|
||||
|
||||
if (isApiOnly) {
|
||||
roles.splice(3, 0, {
|
||||
disabled: false,
|
||||
label: "GitOps",
|
||||
value: "gitops",
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ require (
|
||||
github.com/mattermost/xml-roundtrip-validator v0.0.0-20201213122252-bcd7e1b9601e
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/micromdm/micromdm v1.9.0
|
||||
github.com/micromdm/nanolib v0.2.0
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/mitchellh/gon v0.2.6-0.20231031204852-2d4f161ccecd
|
||||
github.com/mna/redisc v1.3.2
|
||||
@@ -118,7 +119,6 @@ require (
|
||||
go.elastic.co/apm/module/apmsql/v2 v2.6.2
|
||||
go.elastic.co/apm/v2 v2.6.2
|
||||
go.etcd.io/bbolt v1.3.9
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
|
||||
go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.56.0
|
||||
go.opentelemetry.io/otel v1.31.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0
|
||||
@@ -279,7 +279,6 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-tty v0.0.3 // indirect
|
||||
github.com/micromdm/nanolib v0.2.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
@@ -323,6 +322,7 @@ require (
|
||||
github.com/yashtewari/glob-intersection v0.1.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.elastic.co/fastjson v1.1.0 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
|
||||
@@ -846,8 +846,6 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/macadmins/osquery-extension v1.2.1 h1:p7tAAhfEjUjoMQJNb+X7Qc3FraVqGZqMhZ1BYJbrlaw=
|
||||
github.com/macadmins/osquery-extension v1.2.1/go.mod h1:q0BnBuYocHBRB+m3AQwdQNETH5a2KzVT3S8TKMHo9Lk=
|
||||
github.com/macadmins/osquery-extension v1.2.3 h1:PAAQVRBnpOwnzEUROiJbrjDf9RPwcAfJrNAkXUcjS3Y=
|
||||
github.com/macadmins/osquery-extension v1.2.3/go.mod h1:cNd/9INYpAYJFjfmAEJKgiuHgDkGuFMPu6GVrn7oups=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
|
||||
@@ -56,7 +56,7 @@ variable "database_name" {
|
||||
|
||||
variable "fleet_image" {
|
||||
description = "the name of the container image to run"
|
||||
default = "fleetdm/fleet:v4.59.1"
|
||||
default = "fleetdm/fleet:v4.60.0"
|
||||
}
|
||||
|
||||
variable "software_inventory" {
|
||||
|
||||
@@ -68,7 +68,7 @@ variable "redis_mem" {
|
||||
}
|
||||
|
||||
variable "image" {
|
||||
default = "fleetdm/fleet:v4.59.1"
|
||||
default = "fleetdm/fleet:v4.60.0"
|
||||
}
|
||||
|
||||
variable "software_installers_bucket_name" {
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "4.59.1"
|
||||
constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1"
|
||||
version = "4.60.0"
|
||||
constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0"
|
||||
hashes = [
|
||||
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
|
||||
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
|
||||
|
||||
@@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.59.1"
|
||||
version = "~> 4.60.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
|
||||
@@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.59.1"
|
||||
version = "~> 4.60.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "4.59.1"
|
||||
constraints = ">= 3.63.0, ~> 4.59.1"
|
||||
version = "4.60.0"
|
||||
constraints = ">= 3.63.0, ~> 4.60.0"
|
||||
hashes = [
|
||||
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
|
||||
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
|
||||
|
||||
@@ -20,7 +20,7 @@ terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.59.1"
|
||||
version = "~> 4.60.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/aws" {
|
||||
version = "4.59.1"
|
||||
constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.59.1"
|
||||
version = "4.60.0"
|
||||
constraints = ">= 3.0.0, >= 4.8.0, >= 4.9.0, ~> 4.60.0"
|
||||
hashes = [
|
||||
"h1:fuIdjl9f2JEH0TLoq5kc9NIPbJAAV7YBbZ8fvNp5XSg=",
|
||||
"zh:0341a460210463a0bebd5c12ce13dc49bd8cae2399b215418c5efa607fed84e4",
|
||||
|
||||
@@ -15,7 +15,7 @@ terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.59.1"
|
||||
version = "~> 4.60.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
|
||||
@@ -2,7 +2,7 @@ terraform {
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 4.59.1"
|
||||
version = "~> 4.60.0"
|
||||
}
|
||||
}
|
||||
backend "s3" {
|
||||
|
||||
@@ -1316,19 +1316,6 @@ func (man Manager) addConfigs() {
|
||||
"calendar.periodicity", 0,
|
||||
"How much time to wait between processing calendar integration.",
|
||||
)
|
||||
|
||||
// Hide Microsoft/Windows MDM flags as we don't want it to be discoverable for users for now
|
||||
betaMDMFlags := []string{
|
||||
"mdm.windows_wstep_identity_cert",
|
||||
"mdm.windows_wstep_identity_key",
|
||||
"mdm.windows_wstep_identity_cert_bytes",
|
||||
"mdm.windows_wstep_identity_key_bytes",
|
||||
}
|
||||
for _, mdmFlag := range betaMDMFlags {
|
||||
if flag := man.command.PersistentFlags().Lookup(flagNameFromConfigKey(mdmFlag)); flag != nil {
|
||||
flag.Hidden = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (man Manager) hideConfig(name string) {
|
||||
|
||||
@@ -51,7 +51,7 @@ import (
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
"github.com/groob/plist"
|
||||
"go.mozilla.org/pkcs7"
|
||||
"github.com/smallstep/pkcs7"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -38,9 +38,9 @@ import (
|
||||
"github.com/groob/plist"
|
||||
"github.com/jmoiron/sqlx"
|
||||
micromdm "github.com/micromdm/micromdm/mdm/mdm"
|
||||
"github.com/smallstep/pkcs7"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.mozilla.org/pkcs7"
|
||||
)
|
||||
|
||||
type profileAssignmentReq struct {
|
||||
|
||||
@@ -24,7 +24,7 @@ variable "fleet_config" {
|
||||
vuln_processing_cpu = optional(number, 2048)
|
||||
vuln_data_stream_mem = optional(number, 1024)
|
||||
vuln_data_stream_cpu = optional(number, 512)
|
||||
image = optional(string, "fleetdm/fleet:v4.59.1")
|
||||
image = optional(string, "fleetdm/fleet:v4.60.0")
|
||||
family = optional(string, "fleet-vuln-processing")
|
||||
sidecars = optional(list(any), [])
|
||||
extra_environment_variables = optional(map(string), {})
|
||||
@@ -82,7 +82,7 @@ variable "fleet_config" {
|
||||
vuln_processing_cpu = 2048
|
||||
vuln_data_stream_mem = 1024
|
||||
vuln_data_stream_cpu = 512
|
||||
image = "fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.60.0"
|
||||
family = "fleet-vuln-processing"
|
||||
sidecars = []
|
||||
extra_environment_variables = {}
|
||||
|
||||
@@ -16,7 +16,7 @@ variable "fleet_config" {
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
pid_mode = optional(string, null)
|
||||
image = optional(string, "fleetdm/fleet:v4.59.1")
|
||||
image = optional(string, "fleetdm/fleet:v4.60.0")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
@@ -119,7 +119,7 @@ variable "fleet_config" {
|
||||
mem = 512
|
||||
cpu = 256
|
||||
pid_mode = null
|
||||
image = "fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.60.0"
|
||||
family = "fleet"
|
||||
sidecars = []
|
||||
depends_on = []
|
||||
|
||||
@@ -77,7 +77,7 @@ variable "fleet_config" {
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
pid_mode = optional(string, null)
|
||||
image = optional(string, "fleetdm/fleet:v4.59.1")
|
||||
image = optional(string, "fleetdm/fleet:v4.60.0")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
@@ -205,7 +205,7 @@ variable "fleet_config" {
|
||||
mem = 512
|
||||
cpu = 256
|
||||
pid_mode = null
|
||||
image = "fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.60.0"
|
||||
family = "fleet"
|
||||
sidecars = []
|
||||
depends_on = []
|
||||
|
||||
@@ -17,7 +17,7 @@ provider "aws" {
|
||||
}
|
||||
|
||||
locals {
|
||||
fleet_image = "fleetdm/fleet:v4.59.1"
|
||||
fleet_image = "fleetdm/fleet:v4.60.0"
|
||||
domain_name = "example.com"
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ variable "fleet_config" {
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
pid_mode = optional(string, null)
|
||||
image = optional(string, "fleetdm/fleet:v4.59.1")
|
||||
image = optional(string, "fleetdm/fleet:v4.60.0")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
@@ -298,7 +298,7 @@ variable "fleet_config" {
|
||||
mem = 512
|
||||
cpu = 256
|
||||
pid_mode = null
|
||||
image = "fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.60.0"
|
||||
family = "fleet"
|
||||
sidecars = []
|
||||
depends_on = []
|
||||
|
||||
@@ -63,8 +63,8 @@ module "fleet" {
|
||||
|
||||
fleet_config = {
|
||||
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
|
||||
# for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.59.1" # override default to deploy the image you desire
|
||||
# for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.60.0"
|
||||
image = "fleetdm/fleet:v4.60.0" # override default to deploy the image you desire
|
||||
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
|
||||
# memory and cpu.
|
||||
autoscaling = {
|
||||
|
||||
@@ -218,7 +218,7 @@ variable "fleet_config" {
|
||||
mem = optional(number, 4096)
|
||||
cpu = optional(number, 512)
|
||||
pid_mode = optional(string, null)
|
||||
image = optional(string, "fleetdm/fleet:v4.59.1")
|
||||
image = optional(string, "fleetdm/fleet:v4.60.0")
|
||||
family = optional(string, "fleet")
|
||||
sidecars = optional(list(any), [])
|
||||
depends_on = optional(list(any), [])
|
||||
@@ -346,7 +346,7 @@ variable "fleet_config" {
|
||||
mem = 512
|
||||
cpu = 256
|
||||
pid_mode = null
|
||||
image = "fleetdm/fleet:v4.59.1"
|
||||
image = "fleetdm/fleet:v4.60.0"
|
||||
family = "fleet"
|
||||
sidecars = []
|
||||
depends_on = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fleetctl",
|
||||
"version": "v4.59.1",
|
||||
"version": "v4.60.0",
|
||||
"description": "Installer for the fleetctl CLI tool",
|
||||
"bin": {
|
||||
"fleetctl": "./run.js"
|
||||
|
||||
Reference in New Issue
Block a user