diff --git a/changes/21775-ui-fleet-maintained-apps b/changes/21775-ui-fleet-maintained-apps new file mode 100644 index 0000000000..3cb4883f9b --- /dev/null +++ b/changes/21775-ui-fleet-maintained-apps @@ -0,0 +1 @@ +- add UI for adding fleet maintained apps diff --git a/frontend/__mocks__/softwareMock.ts b/frontend/__mocks__/softwareMock.ts index fb2fc313ba..a40589a099 100644 --- a/frontend/__mocks__/softwareMock.ts +++ b/frontend/__mocks__/softwareMock.ts @@ -7,6 +7,8 @@ import { ISoftwareTitle, ISoftwareTitleDetails, IAppStoreApp, + IFleetMaintainedApp, + IFleetMaintainedAppDetails, } from "interfaces/software"; import { ISoftwareTitlesResponse, @@ -253,3 +255,37 @@ export const createMockSoftwareTitlesResponse = ( ): ISoftwareTitlesResponse => { return { ...DEFAULT_SOFTWARE_TITLES_RESPONSE_MOCK, ...overrides }; }; + +const DEFAULT_FLEET_MAINTAINED_APPS_MOCK: IFleetMaintainedApp = { + id: 1, + name: "test app", + version: "1.2.3", + platform: "darwin", +}; + +export const createMockFleetMaintainedApp = ( + overrides?: Partial +): IFleetMaintainedApp => { + return { + ...DEFAULT_FLEET_MAINTAINED_APPS_MOCK, + ...overrides, + }; +}; + +const DEFAULT_FLEET_MAINTAINED_APP_DETAILS_MOCK: IFleetMaintainedAppDetails = { + id: 1, + name: "Test app", + version: "1.2.3", + platform: "darwin", + pre_install_script: "SELECT * FROM osquery_info WHERE start_time > 1", + install_script: '#!/bin/sh\n\ninstaller -pkg "$INSTALLER" -target /', + post_install_script: 'echo "Installed"', + uninstall_script: + "#!/bin/sh\n\n# Fleet extracts and saves package IDs\npkg_ids=$PACKAGE_ID", +}; + +export const createMockFleetMaintainedAppDetails = ( + overrides?: Partial +) => { + return { ...DEFAULT_FLEET_MAINTAINED_APP_DETAILS_MOCK, ...overrides }; +}; diff --git a/frontend/components/Modal/Modal.tsx b/frontend/components/Modal/Modal.tsx index d69c10c481..deb2d10c72 100644 --- a/frontend/components/Modal/Modal.tsx +++ b/frontend/components/Modal/Modal.tsx @@ -31,6 +31,11 @@ export interface IModalProps { * @default false */ isContentDisabled?: boolean; + /** `disableClosingModal` can be set to disable the users ability to manually + * close the modal. + * @default false + * */ + disableClosingModal?: boolean; className?: string; } @@ -43,6 +48,7 @@ const Modal = ({ isHidden = false, isLoading = false, isContentDisabled = false, + disableClosingModal = false, className, }: IModalProps): JSX.Element => { useEffect(() => { @@ -52,12 +58,16 @@ const Modal = ({ } }; - document.addEventListener("keydown", closeWithEscapeKey); + if (!disableClosingModal) { + document.addEventListener("keydown", closeWithEscapeKey); + } return () => { - document.removeEventListener("keydown", closeWithEscapeKey); + if (!disableClosingModal) { + document.removeEventListener("keydown", closeWithEscapeKey); + } }; - }, []); + }, [disableClosingModal, onExit]); useEffect(() => { if (onEnter) { @@ -101,11 +111,13 @@ const Modal = ({
{title} -
- -
+ {!disableClosingModal && ( +
+ +
+ )}
diff --git a/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx b/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx index 608ccd91fd..5176b0986f 100644 --- a/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx +++ b/frontend/components/TableContainer/DataTable/HeaderCell/HeaderCell.tsx @@ -1,17 +1,19 @@ -import React from "react"; +import React, { ReactNode } from "react"; import classnames from "classnames"; interface IHeaderCellProps { - value: string | JSX.Element; // either a string or a TooltipWrapper + value: ReactNode; isSortedDesc?: boolean; disableSortBy?: boolean; + tootip?: ReactNode; } const HeaderCell = ({ value, isSortedDesc, disableSortBy, + tootip, }: IHeaderCellProps): JSX.Element => { let sortArrowClass = ""; if (isSortedDesc === undefined) { diff --git a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx index d8301b9b8b..987054b2aa 100644 --- a/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx +++ b/frontend/components/TableContainer/DataTable/SoftwareNameCell/SoftwareNameCell.tsx @@ -46,7 +46,10 @@ const InstallIconWithTooltip = ({ . ) : ( - "Software can be installed on Host details page." + <> + Install manually on Host details page or automatically with + policy automations. + )} @@ -55,8 +58,9 @@ const InstallIconWithTooltip = ({ }; interface ISoftwareNameCellProps { - name: string; - source: string; + name?: string; + source?: string; + /** pass in a `path` that this cell will link to */ path?: string; router?: InjectedRouter; hasPackage?: boolean; diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts index a3e633a8bb..77b4c91767 100644 --- a/frontend/interfaces/software.ts +++ b/frontend/interfaces/software.ts @@ -389,3 +389,21 @@ export const hasHostSoftwareAppLastInstall = ( export const isIpadOrIphoneSoftwareSource = (source: string) => ["ios_apps", "ipados_apps"].includes(source); + +export interface IFleetMaintainedApp { + id: number; + name: string; + version: string; + platform: string; +} + +export interface IFleetMaintainedAppDetails { + id: number; + name: string; + version: string; + platform: string; + pre_install_script: string; // TODO: is this needed? + install_script: string; + post_install_script: string; // TODO: is this needed? + uninstall_script: string; +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx index 8ccb94c784..5e6de90d62 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareAddPage.tsx @@ -98,7 +98,7 @@ const SoftwareAddPage = ({ {React.cloneElement(children, { router, - teamId: location.query.team_id, + currentTeamId: location.query.team_id, })} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/AddFleetAppSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/AddFleetAppSoftwareModal.tsx new file mode 100644 index 0000000000..1c15a1b1b9 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/AddFleetAppSoftwareModal.tsx @@ -0,0 +1,28 @@ +import Modal from "components/Modal"; +import Spinner from "components/Spinner"; +import { noop } from "lodash"; +import React from "react"; + +const baseClass = "add-fleet-app-software-modal"; + +const AddFleetAppSoftwareModal = () => { + return ( + + <> + +

+ Uploading software so that it's available for install. This may + take few minutes. +

+ +
+ ); +}; + +export default AddFleetAppSoftwareModal; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/_styles.scss new file mode 100644 index 0000000000..2a4a528377 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/_styles.scss @@ -0,0 +1,8 @@ +.add-fleet-app-software-modal { + margin-top: 215px; + text-align: center; + + &__spinner { + margin: 0 auto; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/index.ts new file mode 100644 index 0000000000..80f6e83cec --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/AddFleetAppSoftwareModal/index.ts @@ -0,0 +1 @@ +export { default } from "./AddFleetAppSoftwareModal"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx new file mode 100644 index 0000000000..0ebd284397 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx @@ -0,0 +1,157 @@ +import React, { useState } from "react"; + +import Checkbox from "components/forms/fields/Checkbox"; +import TooltipWrapper from "components/TooltipWrapper"; +import RevealButton from "components/buttons/RevealButton"; +import Button from "components/buttons/Button"; + +import AdvancedOptionsFields from "pages/SoftwarePage/components/AdvancedOptionsFields"; + +import { generateFormValidation } from "./helpers"; + +const baseClass = "fleet-app-details-form"; + +export interface IFleetMaintainedAppFormData { + selfService: boolean; + installScript: string; + preInstallQuery?: string; + postInstallScript?: string; + uninstallScript?: string; +} + +export interface IFormValidation { + isValid: boolean; + preInstallQuery?: { isValid: boolean; message?: string }; +} + +interface IFleetAppDetailsFormProps { + defaultInstallScript: string; + defaultPostInstallScript: string; + defaultUninstallScript: string; + showSchemaButton: boolean; + onClickShowSchema: () => void; + onCancel: () => void; + onSubmit: (formData: IFleetMaintainedAppFormData) => void; +} + +const FleetAppDetailsForm = ({ + defaultInstallScript, + defaultPostInstallScript, + defaultUninstallScript, + showSchemaButton, + onClickShowSchema, + onCancel, + onSubmit, +}: IFleetAppDetailsFormProps) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const [formData, setFormData] = useState({ + selfService: false, + preInstallQuery: undefined, + installScript: defaultInstallScript, + postInstallScript: defaultPostInstallScript, + uninstallScript: defaultUninstallScript, + }); + const [formValidation, setFormValidation] = useState({ + isValid: true, + preInstallQuery: { isValid: false }, + }); + + const onChangePreInstallQuery = (value?: string) => { + const newData = { ...formData, preInstallQuery: value }; + setFormData(newData); + setFormValidation(generateFormValidation(newData)); + }; + + const onChangeInstallScript = (value: string) => { + const newData = { ...formData, installScript: value }; + setFormData(newData); + setFormValidation(generateFormValidation(newData)); + }; + + const onChangePostInstallScript = (value?: string) => { + const newData = { ...formData, postInstallScript: value }; + setFormData(newData); + setFormValidation(generateFormValidation(newData)); + }; + + const onChangeUninstallScript = (value?: string) => { + const newData = { ...formData, uninstallScript: value }; + setFormData(newData); + setFormValidation(generateFormValidation(newData)); + }; + + const onToggleSelfServiceCheckbox = (value: boolean) => { + const newData = { ...formData, selfService: value }; + setFormData(newData); + setFormValidation(generateFormValidation(newData)); + }; + + const onSubmitForm = (evt: React.FormEvent) => { + evt.preventDefault(); + onSubmit(formData); + }; + + const isSubmitDisabled = !formValidation.isValid; + + return ( +
+ + + End users can install from Fleet Desktop {">"} Self-service + . + + } + > + Self-service + + +
+ setShowAdvancedOptions(!showAdvancedOptions)} + /> + {showAdvancedOptions && ( + + )} +
+
+ + +
+
+ ); +}; + +export default FleetAppDetailsForm; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/_styles.scss new file mode 100644 index 0000000000..436e3678e8 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/_styles.scss @@ -0,0 +1,19 @@ +.fleet-app-details-form { + + &__advanced-options-section { + display: flex; + flex-direction: column; + gap: $pad-large; + align-items: flex-start; + } + + &__advanced-options-fields { + width: 100%; + } + + &__form-buttons { + display: flex; + flex-direction: row; + gap: $pad-large; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/helpers.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/helpers.ts new file mode 100644 index 0000000000..12ee2c5f9a --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/helpers.ts @@ -0,0 +1,76 @@ +// @ts-ignore +import validateQuery from "components/forms/validators/validate_query"; + +import { + IFleetMaintainedAppFormData, + IFormValidation, +} from "./FleetAppDetailsForm"; + +type IMessageFunc = (formData: IFleetMaintainedAppFormData) => string; +type IValidationMessage = string | IMessageFunc; + +interface IValidation { + name: string; + isValid: (formData: IFleetMaintainedAppFormData) => boolean; + message?: IValidationMessage; +} + +const FORM_VALIDATION_CONFIG: Record< + "preInstallQuery", + { validations: IValidation[] } +> = { + preInstallQuery: { + validations: [ + { + name: "invalidQuery", + isValid: (formData) => { + const query = formData.preInstallQuery; + return ( + query === undefined || query === "" || validateQuery(query).valid + ); + }, + message: (formData) => validateQuery(formData.preInstallQuery).error, + }, + ], + }, +}; + +const getErrorMessage = ( + formData: IFleetMaintainedAppFormData, + message?: IValidationMessage +) => { + if (message === undefined || typeof message === "string") { + return message; + } + return message(formData); +}; + +// eslint-disable-next-line import/prefer-default-export +export const generateFormValidation = ( + formData: IFleetMaintainedAppFormData +) => { + const formValidation: IFormValidation = { + isValid: true, + }; + + Object.keys(FORM_VALIDATION_CONFIG).forEach((key) => { + const objKey = key as keyof typeof FORM_VALIDATION_CONFIG; + const failedValidation = FORM_VALIDATION_CONFIG[objKey].validations.find( + (validation) => !validation.isValid(formData) + ); + + if (!failedValidation) { + formValidation[objKey] = { + isValid: true, + }; + } else { + formValidation.isValid = false; + formValidation[objKey] = { + isValid: false, + message: getErrorMessage(formData, failedValidation.message), + }; + } + }); + + return formValidation; +}; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/index.ts new file mode 100644 index 0000000000..cdeb43cd2f --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/index.ts @@ -0,0 +1 @@ +export { default } from "./FleetAppDetailsForm"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx new file mode 100644 index 0000000000..a97b7a1fc1 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx @@ -0,0 +1,223 @@ +import React, { useContext, useState } from "react"; +import { Location } from "history"; +import { useQuery } from "react-query"; +import { InjectedRouter } from "react-router"; + +import PATHS from "router/paths"; +import { buildQueryStringFromParams } from "utilities/url"; +import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; +import softwareAPI from "services/entities/software"; +import { QueryContext } from "context/query"; +import { AppContext } from "context/app"; +import { NotificationContext } from "context/notification"; +import { getErrorReason } from "interfaces/errors"; +import { Platform, PLATFORM_DISPLAY_NAMES } from "interfaces/platform"; +import useToggleSidePanel from "hooks/useToggleSidePanel"; + +import BackLink from "components/BackLink"; +import MainContent from "components/MainContent"; +import Spinner from "components/Spinner"; +import DataError from "components/DataError"; +import SidePanelContent from "components/SidePanelContent"; +import QuerySidePanel from "components/side_panels/QuerySidePanel"; +import PremiumFeatureMessage from "components/PremiumFeatureMessage"; +import Card from "components/Card"; + +import SoftwareIcon from "pages/SoftwarePage/components/icons/SoftwareIcon"; + +import FleetAppDetailsForm from "./FleetAppDetailsForm"; +import { IFleetMaintainedAppFormData } from "./FleetAppDetailsForm/FleetAppDetailsForm"; +import AddFleetAppSoftwareModal from "./AddFleetAppSoftwareModal"; + +const baseClass = "fleet-maintained-app-details-page"; + +interface ISoftwareSummaryProps { + name: string; + platform: string; + version: string; +} + +const FleetAppSummary = ({ + name, + platform, + version, +}: ISoftwareSummaryProps) => { + return ( + + +
+
{name}
+
+
+ {PLATFORM_DISPLAY_NAMES[platform as Platform]} +
+ • +
+ {version} +
+
+
+
+ ); +}; + +export interface IFleetMaintainedAppDetailsQueryParams { + team_id?: string; +} + +interface IFleetMaintainedAppDetailsRouteParams { + id: string; +} + +interface IFleetMaintainedAppDetailsPageProps { + location: Location; + router: InjectedRouter; + routeParams: IFleetMaintainedAppDetailsRouteParams; +} + +/** This type includes the editable form data as well as the fleet maintained + * app id */ +export type IAddFleetMaintainedData = IFleetMaintainedAppFormData & { + appId: number; +}; + +const FleetMaintainedAppDetailsPage = ({ + location, + router, + routeParams, +}: IFleetMaintainedAppDetailsPageProps) => { + const teamId = location.query.team_id; + const appId = parseInt(routeParams.id, 10); + + const { renderFlash } = useContext(NotificationContext); + const { isPremiumTier } = useContext(AppContext); + const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext( + QueryContext + ); + const { isSidePanelOpen, setSidePanelOpen } = useToggleSidePanel(false); + const [ + showAddFleetAppSoftwareModal, + setShowAddFleetAppSoftwareModal, + ] = useState(false); + + const { data, isLoading, isError } = useQuery( + ["fleet-maintained-app", appId], + () => softwareAPI.getFleetMainainedApp(appId), + { + ...DEFAULT_USE_QUERY_OPTIONS, + enabled: isPremiumTier, + select: (res) => res.fleet_maintained_app, + } + ); + + const onOsqueryTableSelect = (tableName: string) => { + setSelectedOsqueryTable(tableName); + }; + + const backToAddSoftwareUrl = `${ + PATHS.SOFTWARE_ADD_FLEET_MAINTAINED + }?${buildQueryStringFromParams({ team_id: teamId })}`; + + const onCancel = () => { + router.push(backToAddSoftwareUrl); + }; + + const onSubmit = async (formData: IFleetMaintainedAppFormData) => { + // this should not happen but we need to handle the type correctly + if (!teamId) return; + + setShowAddFleetAppSoftwareModal(true); + + try { + await softwareAPI.addFleetMaintainedApp(parseInt(teamId, 10), { + ...formData, + appId, + }); + renderFlash( + "success", + <> + {data?.name} successfully added. + + ); + router.push( + `${PATHS.SOFTWARE_TITLES}?${buildQueryStringFromParams({ + team_id: teamId, + available_for_install: true, + })}` + ); + } catch (error) { + renderFlash("error", getErrorReason(error)); // TODO: handle error messages + } + + setShowAddFleetAppSoftwareModal(false); + }; + + const renderContent = () => { + if (!isPremiumTier) { + return ; + } + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + if (data) { + return ( + <> + +

{data.name}

+
+ + setSidePanelOpen(true)} + onCancel={onCancel} + onSubmit={onSubmit} + /> +
+ + ); + } + + return null; + }; + + return ( + <> + + <>{renderContent()} + + {isPremiumTier && data && isSidePanelOpen && ( + + setSidePanelOpen(false)} + /> + + )} + {showAddFleetAppSoftwareModal && } + + ); +}; + +export default FleetMaintainedAppDetailsPage; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/_styles.scss new file mode 100644 index 0000000000..4995d17283 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/_styles.scss @@ -0,0 +1,31 @@ +.fleet-maintained-app-details-page { + &__back-to-add-software { + margin-bottom: $pad-medium; + } + + h1 { + margin-bottom: $pad-large; + } + + &__page-content { + display: flex; + flex-direction: column; + gap: $pad-large; + } + + &__fleet-app-summary { + display: flex; + gap: $pad-medium; + } + + &__fleet-app-summary--title { + font-weight: $bold; + font-size: $small; + } + + &__fleet-app-summary--info { + font-size: $x-small; + display: flex; + gap: $pad-xsmall; + } +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/index.ts new file mode 100644 index 0000000000..3f81c57288 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/index.ts @@ -0,0 +1 @@ +export { default } from "./FleetMaintainedAppDetailsPage"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx new file mode 100644 index 0000000000..4cdc172704 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTable.tsx @@ -0,0 +1,164 @@ +import React, { useCallback, useMemo } from "react"; +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 TableContainer from "components/TableContainer"; +import TableCount from "components/TableContainer/TableCount"; +import LastUpdatedText from "components/LastUpdatedText"; +import { ITableQueryData } from "components/TableContainer/TableContainer"; + +import { generateTableConfig } from "./FleetMaintainedAppsTableConfig"; + +const baseClass = "fleet-maintained-apps-table"; + +interface IFleetMaintainedAppsTableProps { + teamId: number; + isLoading: boolean; + query: string; + perPage: number; + orderDirection: "asc" | "desc"; + orderKey: string; + currentPage: number; + router: InjectedRouter; + data?: ISoftwareFleetMaintainedAppsResponse; +} + +const FleetMaintainedAppsTable = ({ + teamId, + isLoading, + data, + router, + query, + perPage, + orderDirection, + orderKey, + currentPage, +}: IFleetMaintainedAppsTableProps) => { + const determineQueryParamChange = useCallback( + (newTableQuery: ITableQueryData) => { + const changedEntry = Object.entries(newTableQuery).find(([key, val]) => { + switch (key) { + case "searchQuery": + return val !== query; + case "sortDirection": + return val !== orderDirection; + case "sortHeader": + return val !== orderKey; + case "pageIndex": + return val !== currentPage; + default: + return false; + } + }); + return changedEntry?.[0] ?? ""; + }, + [currentPage, orderDirection, orderKey, query] + ); + + const generateNewQueryParams = useCallback( + (newTableQuery: ITableQueryData, changedParam: string) => { + const newQueryParam: Record = { + query: newTableQuery.searchQuery, + team_id: teamId, + order_direction: newTableQuery.sortDirection, + order_key: newTableQuery.sortHeader, + page: changedParam === "pageIndex" ? newTableQuery.pageIndex : 0, + }; + + return newQueryParam; + }, + [teamId] + ); + + // NOTE: this is called once on initial render and every time the query changes + const onQueryChange = useCallback( + (newTableQuery: ITableQueryData) => { + // we want to determine which query param has changed in order to + // reset the page index to 0 if any other param has changed. + const changedParam = determineQueryParamChange(newTableQuery); + + // if nothing has changed, don't update the route. this can happen when + // this handler is called on the inital render. Can also happen when + // the filter dropdown is changed. That is handled on the onChange handler + // for the dropdown. + if (changedParam === "") return; + + const newRoute = getNextLocationPath({ + pathPrefix: PATHS.SOFTWARE_ADD_FLEET_MAINTAINED, + routeTemplate: "", + queryParams: generateNewQueryParams(newTableQuery, changedParam), + }); + + router.replace(newRoute); + }, + [determineQueryParamChange, generateNewQueryParams, router] + ); + + const handleRowClick = () => { + // TODO: change to correct path + const path = `${PATHS.MANAGE_HOSTS}?${buildQueryStringFromParams({ + team_id: teamId, + })}`; + + router.push(path); + }; + + const tableHeadersConfig = useMemo(() => { + if (!data) return []; + return generateTableConfig(router, teamId); + }, [data, router, teamId]); + + const renderCount = () => { + if (!data) return null; + + return ( + <> + + {data?.counts_updated_at && ( + + The last time Fleet-maintained
+ package library data was
+ updated. + + } + /> + )} + + ); + }; + + return ( + + className={baseClass} + columnConfigs={tableHeadersConfig} + data={data?.fleet_maintained_apps ?? []} + isLoading={isLoading} + resultsTitle="items" + emptyComponent={() => <>EMPTY STATE TODO} + defaultSortHeader={orderKey} + defaultSortDirection={orderDirection} + defaultPageIndex={currentPage} + defaultSearchQuery={query} + manualSortBy + pageSize={perPage} + showMarkAllPages={false} + isAllPagesSelected={false} + disableNextPage={!data?.meta.has_next_results} + searchable + inputPlaceHolder="Search by name" + onQueryChange={onQueryChange} + renderCount={renderCount} + disableMultiRowSelect + onClickRow={handleRowClick} + /> + ); +}; + +export default FleetMaintainedAppsTable; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx new file mode 100644 index 0000000000..42f2012a87 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/FleetMaintainedAppsTableConfig.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { Column } from "react-table"; +import { InjectedRouter } from "react-router"; + +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 TextCell from "components/TableContainer/DataTable/TextCell"; +import HeaderCell from "components/TableContainer/DataTable/HeaderCell"; +import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell"; +import TooltipWrapper from "components/TooltipWrapper"; + +type IFleetMaintainedAppsTableConfig = Column; +type ITableStringCellProps = IStringCellProps; +type ITableHeaderProps = IHeaderProps; + +// eslint-disable-next-line import/prefer-default-export +export const generateTableConfig = ( + router: InjectedRouter, + teamId: number +): IFleetMaintainedAppsTableConfig[] => { + return [ + { + Header: (cellProps: ITableHeaderProps) => ( + + ), + accessor: "name", + Cell: (cellProps: ITableStringCellProps) => { + const { name, id } = cellProps.row.original; + + const path = `${PATHS.SOFTWARE_FLEET_MAINTAINED_DETAILS( + id + )}?${buildQueryStringFromParams({ + team_id: teamId, + })}`; + + return ; + }, + sortType: "caseInsensitive", + }, + { + Header: "Version", + accessor: "version", + Cell: ({ cell }: ITableStringCellProps) => ( + + ), + disableSortBy: true, + }, + { + Header: () => { + const titleWithToolTip = ( + + Currently, only macOS apps are
+ supported. + + } + > + Platform +
+ ); + return ; + }, + accessor: "platform", + Cell: ({ cell }: ITableStringCellProps) => ( + + ), + disableSortBy: true, + }, + ]; +}; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss new file mode 100644 index 0000000000..2238878880 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/_styles.scss @@ -0,0 +1,3 @@ +.fleet-maintained-apps-table { + +} diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/index.ts b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/index.ts new file mode 100644 index 0000000000..a23cb12090 --- /dev/null +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppsTable/index.ts @@ -0,0 +1 @@ +export { default } from "./FleetMaintainedAppsTable"; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/SoftwareFleetMaintained.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/SoftwareFleetMaintained.tsx index aaa9627bdd..dcb87bd3b6 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/SoftwareFleetMaintained.tsx +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/SoftwareFleetMaintained.tsx @@ -1,9 +1,15 @@ import React from "react"; import { InjectedRouter } from "react-router"; import { Location } from "history"; +import { useQuery } from "react-query"; -import { DEFAULT_QUERY } from "utilities/constants"; +import softwareAPI from "services/entities/software"; +import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; +import Spinner from "components/Spinner"; +import DataError from "components/DataError"; + +import FleetMaintainedAppsTable from "./FleetMaintainedAppsTable"; import { ISoftwareAddPageQueryParams } from "../SoftwareAddPage"; const baseClass = "software-fleet-maintained"; @@ -15,8 +21,8 @@ interface ISoftwareFleetMaintainedProps { } // default values for query params used on this page if not provided -const DEFAULT_SORT_DIRECTION = "desc"; -const DEFAULT_SORT_HEADER = "hosts_count"; +const DEFAULT_SORT_DIRECTION = "asc"; +const DEFAULT_SORT_HEADER = "name"; const DEFAULT_PAGE_SIZE = 20; const DEFAULT_PAGE = 0; @@ -28,12 +34,42 @@ const SoftwareFleetMaintained = ({ const { order_key = DEFAULT_SORT_HEADER, order_direction = DEFAULT_SORT_DIRECTION, - query = DEFAULT_QUERY, + query = "", page, } = location.query; const currentPage = page ? parseInt(page, 10) : DEFAULT_PAGE; - return
Maintained Page
; + const { data, isLoading, isError } = useQuery( + ["fleet-maintained", currentTeamId], + () => softwareAPI.getFleetMaintainedApps(currentTeamId), + { + ...DEFAULT_USE_QUERY_OPTIONS, + } + ); + + if (isLoading) { + return ; + } + + if (isError) { + return ; + } + + return ( +
+ +
+ ); }; export default SoftwareFleetMaintained; diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/_styles.scss b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/_styles.scss index a3e72e4c24..11fd431079 100644 --- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/_styles.scss +++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/_styles.scss @@ -1,3 +1,5 @@ .software-fleet-maintained { - + &__table-error { + margin-top: $pad-xxxlarge; + } } diff --git a/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx new file mode 100644 index 0000000000..95bf94be5e --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/AdvancedOptionsFields.tsx @@ -0,0 +1,112 @@ +import React, { ReactNode } from "react"; +import classnames from "classnames"; + +import Editor from "components/Editor"; +import FleetAce from "components/FleetAce"; +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; + +const baseClass = "advanced-options-fields"; + +interface IAdvancedOptionsFieldsProps { + showSchemaButton: boolean; + installScriptHelpText: ReactNode; + postInstallScriptHelpText: ReactNode; + uninstallScriptHelpText: ReactNode; + errors: { preInstallQuery?: string; postInstallScript?: string }; + preInstallQuery?: string; + installScript: string; + postInstallScript?: string; + uninstallScript?: string; + className?: string; + onClickShowSchema: () => void; + onChangePreInstallQuery: (value?: string) => void; + onChangeInstallScript: (value: string) => void; + onChangePostInstallScript: (value?: string) => void; + onChangeUninstallScript: (value?: string) => void; +} + +const AdvancedOptionsFields = ({ + showSchemaButton, + installScriptHelpText, + postInstallScriptHelpText, + uninstallScriptHelpText, + errors, + preInstallQuery, + installScript, + postInstallScript, + uninstallScript, + className, + onClickShowSchema, + onChangePreInstallQuery, + onChangeInstallScript, + onChangePostInstallScript, + onChangeUninstallScript, +}: IAdvancedOptionsFieldsProps) => { + const classNames = classnames(baseClass, className); + + const renderLabelComponent = (): JSX.Element | null => { + if (showSchemaButton) { + return null; + } + + return ( + + ); + }; + + return ( +
+ + + + +
+ ); +}; + +export default AdvancedOptionsFields; diff --git a/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/_styles.scss b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/_styles.scss new file mode 100644 index 0000000000..cdac97da17 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/_styles.scss @@ -0,0 +1,5 @@ +.advanced-options-fields { + display: flex; + flex-direction: column; + gap: $pad-medium; +} diff --git a/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/index.ts b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/index.ts new file mode 100644 index 0000000000..666497c59b --- /dev/null +++ b/frontend/pages/SoftwarePage/components/AdvancedOptionsFields/index.ts @@ -0,0 +1 @@ +export { default } from "./AdvancedOptionsFields"; diff --git a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx index ad06e516ec..5758b67b03 100644 --- a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx +++ b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { noop } from "lodash"; import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; @@ -8,11 +9,11 @@ import { PackageType, } from "interfaces/package_type"; -import Editor from "components/Editor"; import CustomLink from "components/CustomLink"; -import FleetAce from "components/FleetAce"; import RevealButton from "components/buttons/RevealButton"; + import { IPackageFormData } from "../PackageForm/PackageForm"; +import AdvancedOptionsFields from "../AdvancedOptionsFields"; const getSupportedScriptTypeText = (pkgType: PackageType) => { return `Currently, ${ @@ -95,63 +96,23 @@ const PackageAdvancedOptions = ({ return null; } return ( -
- - Software will be installed only if the{" "} - - - } - /> - - - -
+ ); }; diff --git a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/_styles.scss b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/_styles.scss index 99167137da..c31242eaf8 100644 --- a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/_styles.scss +++ b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/_styles.scss @@ -6,9 +6,6 @@ &__input-fields { width: 100%; - display: flex; - flex-direction: column; - gap: $pad-medium; } &__table-link { diff --git a/frontend/pages/SoftwarePage/components/icons/Box.tsx b/frontend/pages/SoftwarePage/components/icons/Box.tsx new file mode 100644 index 0000000000..005f142630 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Box.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Box = (props: SVGProps) => ( + + + + +); +export default Box; diff --git a/frontend/pages/SoftwarePage/components/icons/Brave.tsx b/frontend/pages/SoftwarePage/components/icons/Brave.tsx new file mode 100644 index 0000000000..a75ddc94bf --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Brave.tsx @@ -0,0 +1,61 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Brave = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + +); +export default Brave; diff --git a/frontend/pages/SoftwarePage/components/icons/Cloudflare.tsx b/frontend/pages/SoftwarePage/components/icons/Cloudflare.tsx new file mode 100644 index 0000000000..1cf10d2dee --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Cloudflare.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Cloudflare = (props: SVGProps) => ( + + + + + +); +export default Cloudflare; diff --git a/frontend/pages/SoftwarePage/components/icons/Docker.tsx b/frontend/pages/SoftwarePage/components/icons/Docker.tsx new file mode 100644 index 0000000000..0510385110 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Docker.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Docker = (props: SVGProps) => ( + + + + +); +export default Docker; diff --git a/frontend/pages/SoftwarePage/components/icons/Edge.tsx b/frontend/pages/SoftwarePage/components/icons/Edge.tsx new file mode 100644 index 0000000000..726bb1a70e --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Edge.tsx @@ -0,0 +1,116 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Edge = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +); +export default Edge; diff --git a/frontend/pages/SoftwarePage/components/icons/Figma.tsx b/frontend/pages/SoftwarePage/components/icons/Figma.tsx new file mode 100644 index 0000000000..91f721bde9 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Figma.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Figma = (props: SVGProps) => ( + + + + + + + + + + + + + + + +); +export default Figma; diff --git a/frontend/pages/SoftwarePage/components/icons/Notion.tsx b/frontend/pages/SoftwarePage/components/icons/Notion.tsx new file mode 100644 index 0000000000..bae121084a --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Notion.tsx @@ -0,0 +1,20 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const Notion = (props: SVGProps) => ( + + + + + +); +export default Notion; diff --git a/frontend/pages/SoftwarePage/components/icons/TeamViewer.tsx b/frontend/pages/SoftwarePage/components/icons/TeamViewer.tsx new file mode 100644 index 0000000000..4c50e80469 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/TeamViewer.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const TeamViewer = (props: SVGProps) => ( + + + + + + + + + + + + + + + + + + + +); +export default TeamViewer; diff --git a/frontend/pages/SoftwarePage/components/icons/TeamViewerHost.tsx b/frontend/pages/SoftwarePage/components/icons/TeamViewerHost.tsx new file mode 100644 index 0000000000..42632e584a --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/TeamViewerHost.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const AppStore = (props: SVGProps) => ( + + + + +); +export default AppStore; diff --git a/frontend/pages/SoftwarePage/components/icons/WhatsApp.tsx b/frontend/pages/SoftwarePage/components/icons/WhatsApp.tsx new file mode 100644 index 0000000000..a839380f19 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/WhatsApp.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const WhatsApp = (props: SVGProps) => ( + + + + + +); +export default WhatsApp; diff --git a/frontend/pages/SoftwarePage/components/icons/WindowsDefender.tsx b/frontend/pages/SoftwarePage/components/icons/WindowsDefender.tsx new file mode 100644 index 0000000000..1dbab8f73c --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/WindowsDefender.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +import type { SVGProps } from "react"; + +const WindowsDefender = (props: SVGProps) => ( + + + + +); +export default WindowsDefender; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index 2c8d355f7e..df12980539 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -23,6 +23,16 @@ import Falcon from "./Falcon"; import AppStore from "./AppStore"; import iOS from "./iOS"; import iPadOS from "./iPadOS"; +import TeamViewer from "./TeamViewer"; +import Box from "./Box"; +import Brave from "./Brave"; +import Cloudflare from "./Cloudflare"; +import Docker from "./Docker"; +import Edge from "./Edge"; +import Figma from "./Figma"; +import Notion from "./Notion"; +import WindowsDefender from "./WindowsDefender"; +import WhatsApp from "./WhatsApp"; // Maps all known Linux platforms to the LinuxOS icon const LINUX_OS_NAME_TO_ICON_MAP = HOST_LINUX_PLATFORMS.reduce( @@ -51,6 +61,16 @@ const SOFTWARE_NAME_TO_ICON_MAP = { chrome: ChromeOS, ios: iOS, ipados: iPadOS, + whatsapp: WhatsApp, + notion: Notion, + figma: Figma, + edge: Edge, + docker: Docker, + cloudflare: Cloudflare, + brave: Brave, + box: Box, + "team viewer": TeamViewer, + "windows defender": WindowsDefender, ...LINUX_OS_NAME_TO_ICON_MAP, } as const; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 7967069567..6997039703 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -81,6 +81,7 @@ import SoftwareAddPage from "pages/SoftwarePage/SoftwareAddPage"; import SoftwareFleetMaintained from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained"; import SoftwarePackage from "pages/SoftwarePage/SoftwareAddPage/SoftwarePackage"; import SoftwareAppStore from "pages/SoftwarePage/SoftwareAddPage/SoftwareAppStore"; +import FleetMaintainedAppDetailsPage from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage"; import PATHS from "router/paths"; @@ -286,6 +287,10 @@ const routes = ( + diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index 3d76ced95b..6da5a9ca2b 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -76,6 +76,8 @@ export default { return `${URL_PREFIX}/software/vulnerabilities/${cve}`; }, SOFTWARE_ADD_FLEET_MAINTAINED: `${URL_PREFIX}/software/add/fleet-maintained`, + SOFTWARE_FLEET_MAINTAINED_DETAILS: (id: number) => + `${URL_PREFIX}/software/add/fleet-maintained/${id}`, SOFTWARE_ADD_PACKAGE: `${URL_PREFIX}/software/add/package`, SOFTWARE_ADD_APP_STORE: `${URL_PREFIX}/software/add/app-store`, diff --git a/frontend/services/entities/software.ts b/frontend/services/entities/software.ts index b244f6de4d..47e3076d55 100644 --- a/frontend/services/entities/software.ts +++ b/frontend/services/entities/software.ts @@ -6,12 +6,19 @@ import { ISoftwareVersion, ISoftwareTitle, ISoftwareTitleDetails, + IFleetMaintainedApp, + IFleetMaintainedAppDetails, } from "interfaces/software"; import { buildQueryStringFromParams, convertParamsToSnakeCase, } from "utilities/url"; import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/PackageForm"; +import { + createMockFleetMaintainedApp, + createMockFleetMaintainedAppDetails, +} from "__mocks__/softwareMock"; +import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage"; export interface ISoftwareApiParams { page?: number; @@ -102,6 +109,29 @@ export interface ISoftwareInstallTokenResponse { token: string; } +export interface ISoftwareFleetMaintainedAppsResponse { + fleet_maintained_apps: IFleetMaintainedApp[]; + count: number; + counts_updated_at: string | null; + meta: { + has_next_results: boolean; + has_previous_results: boolean; + }; +} + +export interface IFleetMaintainedAppResponse { + fleet_maintained_app: IFleetMaintainedAppDetails; +} + +interface IAddFleetMaintainedAppPostBody { + team_id: number; + fleet_maintained_app_id: number; + pre_install_query?: string; + install_script?: string; + post_install_script?: string; + self_service?: boolean; +} + const ORDER_KEY = "name"; const ORDER_DIRECTION = "asc"; @@ -286,4 +316,62 @@ export default { const path = SOFTWARE_INSTALL_RESULTS(installUuid); return sendRequest("GET", path); }, + + getFleetMaintainedApps: ( + teamId: number + ): Promise => { + const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints; + const path = `${SOFTWARE_FLEET_MAINTAINED_APPS}?team_id=${teamId}`; + + return new Promise((resolve) => { + resolve({ + fleet_maintained_apps: [ + createMockFleetMaintainedApp({ + name: "edge", + }), + ], + count: 1, + counts_updated_at: "2021-09-01T00:00:00Z", + meta: { + has_next_results: false, + has_previous_results: false, + }, + }); + }); + + // return sendRequest("GET", path); + }, + + getFleetMainainedApp: (id: number): Promise => { + const { SOFTWARE_FLEET_MAINTAINED_APP } = endpoints; + const path = `${SOFTWARE_FLEET_MAINTAINED_APP(id)}`; + + return new Promise((resolve) => { + resolve({ + fleet_maintained_app: createMockFleetMaintainedAppDetails({ + name: "box", + }), + }); + }); + + // return sendRequest("GET", path); + }, + + addFleetMaintainedApp: ( + teamId: number, + formData: IAddFleetMaintainedData + ) => { + const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints; + + const body: IAddFleetMaintainedAppPostBody = { + team_id: teamId, + fleet_maintained_app_id: formData.appId, + pre_install_query: formData.preInstallQuery, + install_script: formData.installScript, + post_install_script: formData.postInstallScript, + self_service: formData.selfService, + }; + + return sendRequest("POST", SOFTWARE_FLEET_MAINTAINED_APPS, body); + }, }; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 29524b4c31..5833a48653 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -174,6 +174,9 @@ export default { `/${API_VERSION}/fleet/software/packages/${id}`, SOFTWARE_AVAILABLE_FOR_INSTALL: (id: number) => `/${API_VERSION}/fleet/software/titles/${id}/available_for_install`, + SOFTWARE_FLEET_MAINTAINED_APPS: `/${API_VERSION}/fleet/software/fleet_maintained_apps`, + SOFTWARE_FLEET_MAINTAINED_APP: (id: number) => + `/${API_VERSION}/fleet/software/fleet_maintained_apps/${id}`, // AI endpoints AUTOFILL_POLICY: `/${API_VERSION}/fleet/autofill/policy`,