add startup assistant to the UI (#17731)

relates to #9147

add the setup assistant page to the UI. This includes:

- new setup assistant page
- uploaded profile and release device manually form
- preview for setup assistant flow

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality

---------

Co-authored-by: Martin Angers <martin.n.angers@gmail.com>
This commit is contained in:
Gabriel Hernandez
2024-03-26 14:46:33 +00:00
committed by GitHub
co-authored by Martin Angers
parent 8253e77264
commit a292e704de
33 changed files with 698 additions and 10 deletions
+1
View File
@@ -145,6 +145,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
bootstrap_package: "",
enable_end_user_authentication: false,
macos_setup_assistant: null,
enable_release_device_manually: false,
},
macos_migration: {
enable: false,
+12 -2
View File
@@ -8,14 +8,20 @@ type CardColor = "white" | "gray" | "purple" | "yellow";
interface ICardProps {
children?: React.ReactNode;
/** The size of the border radius. Defaults to `small` */
/** The size of the border radius. Defaults to `small`. */
borderRadiusSize?: BorderRadiusSize;
/** Includes the card shadows. Defaults to `false` */
includeShadow?: boolean;
/** The color of the card. Defaults to `white` */
color?: CardColor;
className?: string;
/** Increases to 40px padding. Defaults to `false` */
/** The size of the padding around the content of the card. Defaults to `large`.
*
* These correspond to the padding sizes in the design system. Look at `padding.scss` for values */
paddingSize?: "small" | "medium" | "large" | "xlarge" | "xxlarge";
/** NOTE: DEPRICATED. Use `paddingSize` prop instead.
*
* Increases to 40px padding. Defaults to `false` */
largePadding?: boolean;
}
@@ -30,12 +36,16 @@ const Card = ({
color = "white",
className,
largePadding = false,
paddingSize = "large",
}: ICardProps) => {
const classNames = classnames(
baseClass,
`${baseClass}__${color}`,
`${baseClass}__radius-${borderRadiusSize}`,
{
// TODO: simplify this when we've replaced largePadding prop with paddingSize
[`${baseClass}__padding-${paddingSize}`]:
!largePadding && paddingSize !== undefined,
[`${baseClass}__shadow`]: includeShadow,
[`${baseClass}__large-padding`]: largePadding,
},
+22
View File
@@ -22,7 +22,29 @@
box-shadow: $box-shadow;
}
&__padding-small {
padding: $pad-small;
}
&__padding-medium {
padding: $pad-medium;
}
&__padding-large {
padding: $pad-large;
}
&__padding-xlarge {
padding: $pad-xlarge;
}
&__padding-xxlarge {
padding: $pad-xxlarge;
}
// 40px padding
// TODO: remove when we've replaced all instances of largePadding with
// paddingSize prop
&__large-padding {
padding: $pad-xxlarge;
}
@@ -32,6 +32,8 @@ interface IFileUploaderProps {
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
*/
accept?: string;
/** The text to display on the upload button */
buttonMessage?: string;
className?: string;
onFileUpload: (files: FileList | null) => void;
}
@@ -45,6 +47,7 @@ const FileUploader = ({
additionalInfo,
isLoading = false,
accept,
buttonMessage = "Upload",
className,
onFileUpload,
}: IFileUploaderProps) => {
@@ -73,11 +76,11 @@ const FileUploader = ({
variant="brand"
isLoading={isLoading}
>
<label htmlFor="upload-profile">Upload</label>
<label htmlFor="upload-file">{buttonMessage}</label>
</Button>
<input
accept={accept}
id="upload-profile"
id="upload-file"
type="file"
onChange={(e) => {
onFileUpload(e.target.files);
@@ -30,12 +30,12 @@
&__upload-button {
margin-top: 8px;
// we handle the padding in the label so the entire button is clickable
padding: 0;
}
label {
height: 36px;
width: 78.2667px;
padding: $pad-small $pad-medium;
display: flex;
align-items: center;
justify-content: center;
+1
View File
@@ -48,6 +48,7 @@ export interface IMdmConfig {
bootstrap_package: string | null;
enable_end_user_authentication: boolean;
macos_setup_assistant: string | null;
enable_release_device_manually: boolean | null;
};
macos_migration: IMacOsMigrationSettings;
windows_updates: {
+2 -1
View File
@@ -56,7 +56,8 @@ export interface ITeam extends ITeamSummary {
macos_setup: {
bootstrap_package: string | null;
enable_end_user_authentication: boolean;
macos_setup_assistant: string | null; // TODO: types?
macos_setup_assistant: string | null;
enable_release_device_manually: boolean | null;
};
windows_updates: {
deadline_days: number | null;
@@ -4,6 +4,7 @@ import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication";
import BootstrapPackage from "./cards/BootstrapPackage";
import SetupAssistant from "./cards/SetupAssistant";
interface ISetupExperienceCardProps {
currentTeamId?: number;
@@ -25,6 +26,12 @@ const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<
path: PATHS.CONTROLS_BOOTSTRAP_PACKAGE,
Card: BootstrapPackage,
},
{
title: "Setup assistant",
urlSection: "setup-assistant",
path: PATHS.CONTROLS_SETUP_ASSITANT,
Card: SetupAssistant,
},
];
export default SETUP_EXPERIENCE_NAV_ITEMS;
@@ -6,7 +6,9 @@
font-size: $x-small;
h2 {
font-size: $x-small; // needed to override global h2 style
margin: 0;
font-size: $small;
font-weight: normal;
}
&__preview-img {
@@ -0,0 +1,142 @@
import React, { useState } from "react";
import { useQuery } from "react-query";
import { AxiosError } from "axios";
import { IConfig } from "interfaces/config";
import { API_NO_TEAM_ID, ITeamConfig } from "interfaces/team";
import configAPI from "services/entities/config";
import teamsAPI, { ILoadTeamResponse } from "services/entities/teams";
import mdmAPI, {
IAppleSetupEnrollmentProfileResponse,
} from "services/entities/mdm";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import SectionHeader from "components/SectionHeader";
import Spinner from "components/Spinner";
import CustomLink from "components/CustomLink";
import SetupAssistantPreview from "./components/SetupAssistantPreview";
import SetupAssistantProfileUploader from "./components/SetupAssistantProfileUploader";
import SetuAssistantProfileCard from "./components/SetupAssistantProfileCard/SetupAssistantProfileCard";
import DeleteAutoEnrollmentProfile from "./components/DeleteAutoEnrollmentProfile";
import AdvancedOptionsForm from "./components/AdvancedOptionsForm";
const baseClass = "setup-assistant";
interface ISetupAssistantProps {
currentTeamId: number;
}
const SetupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
const { data: globalConfig, isLoading: isLoadingGlobalConfig } = useQuery<
IConfig,
Error
>(["config", currentTeamId], () => configAPI.loadAll(), {
...DEFAULT_USE_QUERY_OPTIONS,
retry: false,
enabled: currentTeamId === API_NO_TEAM_ID,
});
const { data: teamConfig, isLoading: isLoadingTeamConfig } = useQuery<
ILoadTeamResponse,
Error,
ITeamConfig
>(["team", currentTeamId], () => teamsAPI.load(currentTeamId), {
...DEFAULT_USE_QUERY_OPTIONS,
refetchOnWindowFocus: false,
retry: false,
enabled: currentTeamId !== API_NO_TEAM_ID,
select: (res) => res.team,
});
const {
data: enrollmentProfileData,
isLoading: isLoadingEnrollmentProfile,
error: enrollmentProfileError,
refetch: refetchEnrollmentProfile,
} = useQuery<IAppleSetupEnrollmentProfileResponse, AxiosError>(
["enrollment_profile", currentTeamId],
() => mdmAPI.getSetupEnrollmentProfile(currentTeamId),
{
...DEFAULT_USE_QUERY_OPTIONS,
retry: false,
}
);
const getReleaseDeviceSetting = () => {
if (currentTeamId === API_NO_TEAM_ID) {
return (
globalConfig?.mdm.macos_setup.enable_release_device_manually || false
);
}
return teamConfig?.mdm?.macos_setup.enable_release_device_manually || false;
};
const onUpload = () => {
refetchEnrollmentProfile();
};
const onDelete = () => {
setShowDeleteProfileModal(false);
refetchEnrollmentProfile();
};
const defaultReleaseDeviceSetting = getReleaseDeviceSetting();
const isLoading =
isLoadingGlobalConfig || isLoadingTeamConfig || isLoadingEnrollmentProfile;
const enrollmentProfileNotFound = enrollmentProfileError?.status === 404;
return (
<div className={baseClass}>
<SectionHeader title="Setup assistant" />
{isLoading ? (
<Spinner />
) : (
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__upload-container`}>
<p className={`${baseClass}__section-description`}>
Add an automatic enrollment profile to customize the macOS Setup
Assistant.
<CustomLink
url="https://fleetdm.com/learn-more-about/setup-assistant"
text="Learn how"
newTab
/>
</p>
{enrollmentProfileNotFound || !enrollmentProfileData ? (
<SetupAssistantProfileUploader
currentTeamId={currentTeamId}
onUpload={onUpload}
/>
) : (
<SetuAssistantProfileCard
profile={enrollmentProfileData}
onDelete={() => setShowDeleteProfileModal(true)}
/>
)}
<AdvancedOptionsForm
key={String(defaultReleaseDeviceSetting)}
currentTeamId={currentTeamId}
defaultReleaseDevice={defaultReleaseDeviceSetting}
/>
</div>
<div className={`${baseClass}__preview-container`}>
<SetupAssistantPreview />
</div>
</div>
)}
{showDeleteProfileModal && (
<DeleteAutoEnrollmentProfile
currentTeamId={currentTeamId}
onDelete={onDelete}
onCancel={() => setShowDeleteProfileModal(false)}
/>
)}
</div>
);
};
export default SetupAssistant;
@@ -0,0 +1,25 @@
.setup-assistant {
&__content {
max-width: $break-xxl;
margin: 0 auto;
display: flex;
justify-content: space-between;
gap: $pad-xxlarge;
}
&__upload-container {
display: flex;
flex-direction: column;
gap: $pad-large;
}
&__section-description {
margin: 0;
}
@media (max-width: $break-md) {
&__content {
flex-direction: column;
}
}
}
@@ -0,0 +1,71 @@
import React, { useContext, useState } from "react";
import mdmAPI from "services/entities/mdm";
import TooltipWrapper from "components/TooltipWrapper";
import Checkbox from "components/forms/fields/Checkbox";
import Button from "components/buttons/Button";
import { NotificationContext } from "context/notification";
import RevealButton from "components/buttons/RevealButton";
const baseClass = "advanced-options-form";
interface IAdvancedOptionsFormProps {
currentTeamId: number;
defaultReleaseDevice: boolean;
}
const AdvancedOptionsForm = ({
currentTeamId,
defaultReleaseDevice,
}: IAdvancedOptionsFormProps) => {
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
const [releaseDevice, setReleaseDevice] = useState(defaultReleaseDevice);
const { renderFlash } = useContext(NotificationContext);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
await mdmAPI.updateReleaseDeviceSetting(currentTeamId, releaseDevice);
renderFlash("success", "Successfully updated.");
} catch {
renderFlash("error", "Something went wrong. Please try again.");
}
};
const tooltip = (
<>
When enabled, you&apos;re responsible for sending the DeviceConfigured
command. (Default: <b>Off</b>)
</>
);
return (
<div className={baseClass}>
<RevealButton
className={`${baseClass}__accordion-title`}
isShowing={showAdvancedOptions}
showText="Hide advanced options"
hideText="Show advanced options"
caretPosition="after"
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
/>
{showAdvancedOptions && (
<form onSubmit={handleSubmit}>
<Checkbox
value={releaseDevice}
onChange={() => setReleaseDevice(!releaseDevice)}
>
<TooltipWrapper tipContent={tooltip}>
Release device manually
</TooltipWrapper>
</Checkbox>
<Button type="submit">Save</Button>
</form>
)}
</div>
);
};
export default AdvancedOptionsForm;
@@ -0,0 +1,10 @@
.advanced-options-form {
display: flex;
flex-direction: column;
gap: $pad-large;
&__accordion-title {
// use this so we dont center the text.
justify-content: normal;
}
}
@@ -0,0 +1 @@
export { default } from "./AdvancedOptionsForm";
@@ -0,0 +1,59 @@
import React, { useContext } from "react";
import mdmAPI from "services/entities/mdm";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import { NotificationContext } from "context/notification";
interface DeleteAutoEnrollProfileProps {
currentTeamId: number;
onCancel: () => void;
onDelete: () => void;
}
const baseClass = "delete-auto-enrollment-profile-modal";
const DeleteAutoEnrollProfile = ({
currentTeamId,
onCancel,
onDelete,
}: DeleteAutoEnrollProfileProps) => {
const { renderFlash } = useContext(NotificationContext);
const handleDelete = async () => {
try {
await mdmAPI.deleteSetupEnrollmentProfile(currentTeamId);
renderFlash("success", "Successfully deleted!");
} catch {
renderFlash("error", "Couldnt delete. Please try again.");
}
onDelete();
};
return (
<Modal
className={baseClass}
title="Delete automatic enrollment profile"
onExit={onCancel}
>
<>
<p>Delete the automatic enrollment profile to upload a new one.</p>
<p>
Without an automatic enrollment profile, new macOS hosts will
automatically enroll with the default setup settings.
</p>
<div className="modal-cta-wrap">
<Button type="button" onClick={handleDelete} variant="alert">
Delete
</Button>
<Button onClick={onCancel} variant="inverse-alert">
Cancel
</Button>
</div>
</>
</Modal>
);
};
export default DeleteAutoEnrollProfile;
@@ -0,0 +1 @@
export { default } from "./DeleteAutoEnrollmentProfile";
@@ -0,0 +1,35 @@
import React from "react";
import Card from "components/Card";
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
const baseClass = "setup-assistant-preview";
const SetupAssistantPreview = () => {
return (
<Card
color="gray"
borderRadiusSize="medium"
paddingSize="xxlarge"
className={baseClass}
>
<h2>End user experience</h2>
<p>
After the end user continues past the <b>Remote Management</b> screen,
macOS Setup Assistant displays several screens by default.
</p>
<p>
By adding an automatic enrollment profile you can customize which
screens are displayed and more.
</p>
<img
className={`${baseClass}__preview-img`}
src={OsSetupPreview}
alt="OS setup preview"
/>
</Card>
);
};
export default SetupAssistantPreview;
@@ -0,0 +1,16 @@
.setup-assistant-preview {
font-size: $x-small;
h2 {
margin: 0;
font-size: $small;
font-weight: normal;
}
&__preview-img {
margin-top: $pad-xxlarge;
width: 100%;
display: block;
margin: 40px auto 0;
}
}
@@ -0,0 +1 @@
export { default } from "./SetupAssistantPreview";
@@ -0,0 +1,64 @@
import React from "react";
import FileSaver from "file-saver";
import { uploadedFromNow } from "utilities/date_format";
import Icon from "components/Icon";
import Card from "components/Card";
import Graphic from "components/Graphic";
import Button from "components/buttons/Button";
import { IAppleSetupEnrollmentProfileResponse } from "services/entities/mdm";
const baseClass = "setup-assistant-profile-card";
interface ISetupAssistantProfileCardProps {
profile: IAppleSetupEnrollmentProfileResponse;
onDelete: () => void;
}
const SetupAssistantProfileCard = ({
profile,
onDelete,
}: ISetupAssistantProfileCardProps) => {
const onDownload = () => {
const date = new Date();
const filename = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${
profile.name
}`;
const file = new global.window.File(
[JSON.stringify(profile.enrollment_profile)],
filename
);
FileSaver.saveAs(file);
};
return (
<Card paddingSize="medium" className={baseClass}>
<Graphic name="file-configuration-profile" />
<div className={`${baseClass}__info`}>
<span className={`${baseClass}__profile-name`}>{profile.name}</span>
<span className={`${baseClass}__uploaded-at`}>
{uploadedFromNow(profile.uploaded_at)}
</span>
</div>
<div className={`${baseClass}__actions`}>
<Button
className={`${baseClass}__download-button`}
variant="text-icon"
onClick={onDownload}
>
<Icon name="download" />
</Button>
<Button
className={`${baseClass}__delete-button`}
variant="text-icon"
onClick={onDelete}
>
<Icon name="trash" color="ui-fleet-black-75" />
</Button>
</div>
</Card>
);
};
export default SetupAssistantProfileCard;
@@ -0,0 +1,30 @@
.setup-assistant-profile-card {
display: flex;
gap: $pad-medium;
align-items: center;
// TODO: create reusable list item component and use instead of all these styles.
&__info {
display: flex;
flex-direction: column;
}
&__profile-name {
font-size: $x-small;
font-weight: $bold;
}
&__uploaded-at {
font-size: $xx-small;
}
&__actions {
display: flex;
gap: $pad-medium;
flex: 1;
justify-content: flex-end;
}
&__download-button, &__delete-button {
padding: 11px; // TODO: use a padding value from existing variables. talk to design.
}
}
@@ -0,0 +1 @@
export { default } from "./SetupAssistantProfileCard";
@@ -0,0 +1,62 @@
import React, { useContext, useState } from "react";
import { AxiosResponse } from "axios";
import { IApiError } from "interfaces/errors";
import { NotificationContext } from "context/notification";
import mdmAPI from "services/entities/mdm";
import FileUploader from "components/FileUploader";
import { getErrorMessage } from "./helpers";
const baseClass = "setup-assistant-profile-uploader";
interface ISetupAssistantProfileUploaderProps {
currentTeamId: number;
onUpload: () => void;
}
const SetupAssistantProfileUploader = ({
currentTeamId,
onUpload,
}: ISetupAssistantProfileUploaderProps) => {
const { renderFlash } = useContext(NotificationContext);
const [showLoading, setShowLoading] = useState(false);
const onUploadFile = async (files: FileList | null) => {
setShowLoading(true);
if (!files || files.length === 0) {
setShowLoading(false);
return;
}
const file = files[0];
try {
await mdmAPI.uploadSetupEnrollmentProfile(file, currentTeamId);
renderFlash("success", "Successfully uploaded!");
onUpload();
} catch (e) {
const error = e as AxiosResponse<IApiError>;
const errMessage = getErrorMessage(error);
renderFlash("error", errMessage);
} finally {
setShowLoading(false);
}
};
return (
<FileUploader
message="Automatic enrollment profile (.json)"
graphicName="file-configuration-profile"
accept=".json"
buttonMessage="Add profile"
onFileUpload={onUploadFile}
isLoading={showLoading}
className={baseClass}
/>
);
};
export default SetupAssistantProfileUploader;
@@ -0,0 +1,13 @@
import { getErrorReason } from "interfaces/errors";
const UPLOAD_ERROR_MESSAGES = {
default: {
message: "Couldn't upload. Please try again.",
},
};
// eslint-disable-next-line import/prefer-default-export
export const getErrorMessage = (err: unknown) => {
if (typeof err === "string") return err;
return getErrorReason(err) || UPLOAD_ERROR_MESSAGES.default.message;
};
@@ -0,0 +1 @@
export { default } from "./SetupAssistantProfileUploader";
@@ -0,0 +1 @@
export { default } from "./SetupAssistant";
+1
View File
@@ -14,6 +14,7 @@ export default {
CONTROLS_SETUP_EXPERIENCE: `${URL_PREFIX}/controls/setup-experience`,
CONTROLS_END_USER_AUTHENTICATION: `${URL_PREFIX}/controls/setup-experience/end-user-auth`,
CONTROLS_BOOTSTRAP_PACKAGE: `${URL_PREFIX}/controls/setup-experience/bootstrap-package`,
CONTROLS_SETUP_ASSITANT: `${URL_PREFIX}/controls/setup-experience/setup-assistant`,
CONTROLS_SCRIPTS: `${URL_PREFIX}/controls/scripts`,
DASHBOARD: `${URL_PREFIX}/dashboard`,
+2 -2
View File
@@ -57,8 +57,8 @@ export default {
},
/**
* updateMDMConfig is a special case of update that is used to update the MDM
* config.
* updateMDMConfig is a special case of update that is used to update the app
* MDM config.
*
* If the request fails and `skipParseError` is `true`, the caller is
* responsible for verifying that the value of the rejected promise is an AxiosError
+76
View File
@@ -4,6 +4,7 @@ import {
IMdmProfile,
MdmProfileStatus,
} from "interfaces/mdm";
import { API_NO_TEAM_ID } from "interfaces/team";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url";
@@ -46,6 +47,19 @@ export interface IUploadProfileApiParams {
labels?: string[];
}
interface IUpdateSetupExperienceBody {
team_id?: number;
enable_release_device_manually: boolean;
}
export interface IAppleSetupEnrollmentProfileResponse {
team_id: number | null;
name: string;
uploaded_at: string;
// enrollment profile is an object with keys found here https://developer.apple.com/documentation/devicemanagement/profile.
enrollment_profile: Record<string, any>;
}
const mdmService = {
downloadDeviceUserEnrollmentProfile: (token: string) => {
const { DEVICE_USER_MDM_ENROLLMENT_PROFILE } = endpoints;
@@ -226,6 +240,68 @@ const mdmService = {
enable_end_user_authentication: isEnabled,
});
},
updateReleaseDeviceSetting: (teamId: number, isEnabled: boolean) => {
const { MDM_SETUP_EXPERIENCE } = endpoints;
const body: IUpdateSetupExperienceBody = {
enable_release_device_manually: isEnabled,
};
if (teamId !== API_NO_TEAM_ID) {
body.team_id = teamId;
}
return sendRequest("PATCH", MDM_SETUP_EXPERIENCE, body);
},
getSetupEnrollmentProfile: (teamId?: number) => {
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
if (!teamId || teamId === API_NO_TEAM_ID) {
return sendRequest("GET", MDM_APPLE_SETUP_ENROLLMENT_PROFILE);
}
const path = `${MDM_APPLE_SETUP_ENROLLMENT_PROFILE}?${buildQueryStringFromParams(
{ team_id: teamId }
)}`;
return sendRequest("GET", path);
},
uploadSetupEnrollmentProfile: (file: File, teamId: number) => {
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
const reader = new FileReader();
reader.readAsText(file);
return new Promise((resolve, reject) => {
reader.addEventListener("load", () => {
try {
const body: Record<string, any> = {
name: file.name,
enrollment_profile: JSON.parse(reader.result as string),
};
if (teamId !== API_NO_TEAM_ID) {
body.team_id = teamId;
}
resolve(
sendRequest("POST", MDM_APPLE_SETUP_ENROLLMENT_PROFILE, body)
);
} catch {
// catches invalid JSON
reject("Couldnt upload. The file should include valid JSON.");
}
});
});
},
deleteSetupEnrollmentProfile: (teamId: number) => {
const { MDM_APPLE_SETUP_ENROLLMENT_PROFILE } = endpoints;
if (teamId === API_NO_TEAM_ID) {
return sendRequest("DELETE", MDM_APPLE_SETUP_ENROLLMENT_PROFILE);
}
const path = `${MDM_APPLE_SETUP_ENROLLMENT_PROFILE}?${buildQueryStringFromParams(
{ team_id: teamId }
)}`;
return sendRequest("DELETE", path);
},
};
export default mdmService;
+10
View File
@@ -139,6 +139,16 @@ export default {
return sendRequest("PATCH", path, requestBody);
},
/**
* updates the team config. This can take any partial data that is in the team config.
*/
updateConfig: (data: any, teamId: number): Promise<ITeamConfig> => {
const { TEAMS } = endpoints;
const path = `${TEAMS}/${teamId}`;
return sendRequest("PATCH", path, data);
},
addUsers: (teamId: number | undefined, newUsers: INewTeamUsersBody) => {
if (!teamId || teamId <= API_NO_TEAM_ID) {
return Promise.reject(
@@ -0,0 +1,13 @@
import { uploadedFromNow } from ".";
describe("date_format", () => {
describe("uploadedFromNow util", () => {
it("returns an user friendly uploaded at message", () => {
const currentDate = new Date();
currentDate.setDate(currentDate.getDate() - 2);
const twoDaysAgo = currentDate.toISOString();
expect(uploadedFromNow(twoDaysAgo)).toEqual("Uploaded 2 days ago");
});
});
});
+6
View File
@@ -0,0 +1,6 @@
import { formatDistanceToNow } from "date-fns";
// eslint-disable-next-line import/prefer-default-export
export const uploadedFromNow = (date: string) => {
return `Uploaded ${formatDistanceToNow(new Date(date))} ago`;
};
+2
View File
@@ -79,11 +79,13 @@ export default {
}
return `/api/mdm/apple/enroll?${query}`;
},
MDM_APPLE_SETUP_ENROLLMENT_PROFILE: `/${API_VERSION}/fleet/mdm/apple/enrollment_profile`,
MDM_BOOTSTRAP_PACKAGE_METADATA: (teamId: number) =>
`/${API_VERSION}/fleet/mdm/bootstrap/${teamId}/metadata`,
MDM_BOOTSTRAP_PACKAGE: `/${API_VERSION}/fleet/mdm/bootstrap`,
MDM_BOOTSTRAP_PACKAGE_SUMMARY: `/${API_VERSION}/fleet/mdm/bootstrap/summary`,
MDM_SETUP: `/${API_VERSION}/fleet/mdm/apple/setup`,
MDM_SETUP_EXPERIENCE: `/${API_VERSION}/fleet/setup_experience`,
MDM_EULA: (token: string) => `/${API_VERSION}/fleet/mdm/setup/eula/${token}`,
MDM_EULA_UPLOAD: `/${API_VERSION}/fleet/mdm/setup/eula`,
MDM_EULA_METADATA: `/${API_VERSION}/fleet/mdm/setup/eula/metadata`,