mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
implement UI for uploading, downloading, deleting macOS profiles (#9901)
relates to #9593 Implements the UI for users to upload, download, and delete macos profiles ![image](https://user-images.githubusercontent.com/1153709/219685914-6f44e77b-c2cb-47c3-897d-1ba137510fed.png) - [x] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. - [ ] Added/updated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
7f6a42e4ac
commit
a11e2cce3d
1
changes/issue-9593-implement-profile-UI
Normal file
1
changes/issue-9593-implement-profile-UI
Normal file
@ -0,0 +1 @@
|
||||
- add UI implementation for users to upload, download, and deleted macos profiles.
|
15
frontend/components/icons/Download.tsx
Normal file
15
frontend/components/icons/Download.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
|
||||
const Download = () => {
|
||||
return (
|
||||
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9 1a1 1 0 0 0-2 0v7.365L5.64 7.232a1 1 0 1 0-1.28 1.536l3 2.5a1 1 0 0 0 1.28 0l3-2.5a1 1 0 1 0-1.28-1.536L9 8.365V1ZM2 9a1 1 0 1 0-2 0v6a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V9a1 1 0 1 0-2 0v5H2V9Z"
|
||||
fill="#515774"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
export default Download;
|
41
frontend/components/icons/Profile.tsx
Normal file
41
frontend/components/icons/Profile.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from "react";
|
||||
|
||||
const Profile = () => {
|
||||
return (
|
||||
<svg width="34" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clipPath="url(#a)">
|
||||
<path
|
||||
d="M29.333 39.75H4.667a2.417 2.417 0 0 1-2.417-2.416V2.667A2.417 2.417 0 0 1 4.667.25h19.562c.64 0 1.255.255 1.709.708l5.104 5.105c.453.453.708 1.068.708 1.709v29.562a2.417 2.417 0 0 1-2.417 2.416Z"
|
||||
fill="#fff"
|
||||
stroke="#192147"
|
||||
strokeWidth=".5"
|
||||
/>
|
||||
<g clipPath="url(#b)" fill="#8B8FA2">
|
||||
<path d="M16.999 14.714a6.788 6.788 0 1 0 0 13.575 6.788 6.788 0 0 0 0-13.575Zm0 12.406a5.619 5.619 0 0 1-5.62-5.619 5.619 5.619 0 0 1 5.62-5.62 5.619 5.619 0 0 1 5.62 5.62 5.619 5.619 0 0 1-5.62 5.62Z" />
|
||||
<path d="M16.999 23.207a1.708 1.708 0 1 0 0-3.416 1.708 1.708 0 0 0 0 3.416Z" />
|
||||
<path d="M26.839 22.26c-.933-.417-.933-1.101 0-1.52.933-.419.87-.891-.14-1.054-1.007-.163-1.186-.821-.393-1.466.794-.644.61-1.087-.407-.982-1.017.105-1.359-.486-.759-1.314.598-.829.308-1.208-.646-.843-.954.366-1.439-.118-1.073-1.072.365-.955-.017-1.245-.843-.647-.826.598-1.419.255-1.314-.759.105-1.017-.338-1.198-.982-.407-.645.793-1.305.616-1.466-.393-.16-1.01-.635-1.07-1.054-.14-.417.933-1.1.933-1.52 0-.416-.933-.89-.87-1.054.14-.163 1.01-.821 1.186-1.466.393-.644-.794-1.086-.61-.981.407.104 1.017-.487 1.359-1.315.759-.829-.6-1.208-.308-.842.646.363.955-.12 1.439-1.073 1.073-.954-.363-1.245.016-.647.843.598.828.256 1.419-.759 1.314-1.017-.105-1.198.338-.407.982.794.645.617 1.306-.393 1.466-1.01.16-1.07.635-.14 1.054.931.419.933 1.1 0 1.52-.933.418-.87.89.14 1.054 1.01.163 1.187.821.393 1.465-.793.645-.61 1.087.407.983 1.017-.105 1.36.486.759 1.314-.598.828-.307 1.208.647.843.954-.364 1.438.118 1.073 1.072-.364.954.016 1.245.842.647.826-.598 1.42-.256 1.315.759-.105 1.016.337 1.198.981.407.645-.794 1.306-.617 1.466.393.163 1.01.636 1.07 1.054.14.417-.933 1.101-.933 1.52 0 .416.933.891.87 1.054-.14.163-1.01.821-1.187 1.466-.393.644.793 1.087.61.982-.407-.105-1.017.486-1.36 1.314-.759.829.6 1.208.307.843-.647-.363-.954.119-1.438 1.073-1.072.954.365 1.244-.017.646-.843-.598-.828-.256-1.42.759-1.314 1.017.104 1.198-.338.407-.983-.791-.644-.617-1.305.393-1.465 1.01-.163 1.07-.636.14-1.055Zm-9.84 7.306a8.067 8.067 0 1 1 0-16.132 8.068 8.068 0 0 1 0 16.134v-.002Z" />
|
||||
<path d="m23.195 21.992-5.894-1.317-2.945-4.64-1.31.833 2.945 4.64-1.314 5.892 1.514.34 1.215-5.452 5.452 1.217.337-1.513Z" />
|
||||
</g>
|
||||
<path
|
||||
d="M23.5.5h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
|
||||
fill="#C5C7D1"
|
||||
/>
|
||||
<path
|
||||
d="M24.5.334v5.667c0 .736.597 1.333 1.333 1.333h6"
|
||||
stroke="#192147"
|
||||
strokeWidth=".5"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h34v40H0z" />
|
||||
</clipPath>
|
||||
<clipPath id="b">
|
||||
<path fill="#fff" d="M6.5 11h21v21h-21z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Profile;
|
@ -1,6 +1,11 @@
|
||||
import React from "react";
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
|
||||
const TrashCan = () => {
|
||||
interface ITrashCanProps {
|
||||
color?: Colors;
|
||||
}
|
||||
|
||||
const TrashCan = ({ color = "core-fleet-blue" }: ITrashCanProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
@ -11,7 +16,7 @@ const TrashCan = () => {
|
||||
>
|
||||
<path
|
||||
d="M13.25 2H10.5v-.5A1.5 1.5 0 0 0 9 0H7a1.5 1.5 0 0 0-1.5 1.5V2H2.75c-.69 0-1.25.56-1.25 1.25v1a.5.5 0 0 0 .5.5h12a.5.5 0 0 0 .5-.5v-1c0-.69-.56-1.25-1.25-1.25ZM6.5 1.5A.5.5 0 0 1 7 1h2a.5.5 0 0 1 .5.5V2h-3v-.5ZM2.449 5.75c-.09 0-.16.075-.156.164l.412 8.657A1.498 1.498 0 0 0 4.203 16h7.593c.802 0 1.46-.627 1.498-1.429l.413-8.657a.156.156 0 0 0-.156-.164H2.449ZM9.999 7a.5.5 0 1 1 1 0v6.5a.5.5 0 1 1-1 0V7ZM7.5 7a.5.5 0 1 1 1 0v6.5a.5.5 0 1 1-1 0V7ZM5 7a.5.5 0 1 1 1 0v6.5a.5.5 0 1 1-1 0V7Z"
|
||||
fill="#6a67fe"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
@ -39,6 +39,8 @@ import Clipboard from "./Clipboard";
|
||||
import Eye from "./Eye";
|
||||
import Pencil from "./Pencil";
|
||||
import TrashCan from "./TrashCan";
|
||||
import Profile from "./Profile";
|
||||
import Download from "./Download";
|
||||
|
||||
// a mapping of the usable names of icons to the icon source.
|
||||
export const ICON_MAP = {
|
||||
@ -80,6 +82,8 @@ export const ICON_MAP = {
|
||||
"darwin-purple": ApplePurple,
|
||||
"windows-blue": WindowsBlue,
|
||||
"linux-green": LinuxGreen,
|
||||
profile: Profile,
|
||||
download: Download,
|
||||
};
|
||||
|
||||
export type IconNames = keyof typeof ICON_MAP;
|
||||
|
@ -54,3 +54,16 @@ export interface IMdmSummaryResponse {
|
||||
mobile_device_management_enrollment_status: IMdmStatus;
|
||||
mobile_device_management_solution: IMdmSolution[] | null;
|
||||
}
|
||||
|
||||
export interface IMdmProfile {
|
||||
profile_id: number;
|
||||
team_id: number;
|
||||
name: string;
|
||||
identifier: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface IMdmProfilesResponse {
|
||||
profiles: IMdmProfile[] | null;
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import React from "react";
|
||||
import { Params } from "react-router/lib/Router";
|
||||
|
||||
import SideNav from "pages/admin/components/SideNav";
|
||||
|
||||
import MAC_OS_SETTINGS_NAV_ITEMS from "./MacOSSettingsNavItems";
|
||||
|
||||
const baseClass = "mac-os-settings";
|
||||
|
||||
interface IMacOSSettingsProps {
|
||||
params: Params;
|
||||
}
|
||||
|
||||
const MacOSSettings = ({ params }: IMacOSSettingsProps) => {
|
||||
const { section } = params;
|
||||
const DEFAULT_SETTINGS_SECTION = MAC_OS_SETTINGS_NAV_ITEMS[0];
|
||||
|
||||
const currentFormSection =
|
||||
MAC_OS_SETTINGS_NAV_ITEMS.find((item) => item.urlSection === section) ??
|
||||
DEFAULT_SETTINGS_SECTION;
|
||||
|
||||
const CurrentCard = currentFormSection.Card;
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<SideNav
|
||||
className={`${baseClass}__side-nav`}
|
||||
navItems={MAC_OS_SETTINGS_NAV_ITEMS}
|
||||
activeItem={currentFormSection.urlSection}
|
||||
CurrentCard={<CurrentCard />}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MacOSSettings;
|
@ -0,0 +1,15 @@
|
||||
import PATHS from "router/paths";
|
||||
|
||||
import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
|
||||
import CustomSettings from "./cards/CustomSettings";
|
||||
|
||||
const MAC_OS_SETTINGS_NAV_ITEMS: ISideNavItem<any>[] = [
|
||||
{
|
||||
title: "Custom settings",
|
||||
urlSection: "custom-settings",
|
||||
path: PATHS.CONTROLS_CUSTOM_SETTINGS,
|
||||
Card: CustomSettings,
|
||||
},
|
||||
];
|
||||
|
||||
export default MAC_OS_SETTINGS_NAV_ITEMS;
|
@ -0,0 +1,193 @@
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { format } from "date-fns";
|
||||
import formatDistanceToNow from "date-fns/formatDistanceToNow";
|
||||
import FileSaver from "file-saver";
|
||||
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { IMdmProfile, IMdmProfilesResponse } from "interfaces/mdm";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
import { AppContext } from "context/app";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
|
||||
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
|
||||
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
|
||||
|
||||
const baseClass = "custom-settings";
|
||||
|
||||
const CustomSettings = () => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { currentTeam } = useContext(AppContext);
|
||||
|
||||
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const selectedProfile = useRef<IMdmProfile | null>(null);
|
||||
|
||||
const {
|
||||
data: profiles,
|
||||
error: errorProfiles,
|
||||
refetch: refectchProfiles,
|
||||
} = useQuery<IMdmProfilesResponse, unknown, IMdmProfile[] | null>(
|
||||
["profiles", currentTeam?.id],
|
||||
() => mdmAPI.getProfiles(currentTeam?.id),
|
||||
{
|
||||
select: (data) => data.profiles,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const onClickDownload = async (profile: IMdmProfile) => {
|
||||
const fileContent = await mdmAPI.downloadProfile(profile.profile_id);
|
||||
const formatDate = format(new Date(), "yyyy-MM-dd");
|
||||
const filename = `${formatDate}_${profile.name}.mobileconfig`;
|
||||
const file = new File([fileContent], filename);
|
||||
FileSaver.saveAs(file);
|
||||
};
|
||||
|
||||
const onClickDelete = (profile: IMdmProfile) => {
|
||||
selectedProfile.current = profile;
|
||||
setShowDeleteProfileModal(true);
|
||||
};
|
||||
|
||||
const renderProfiles = () => {
|
||||
if (!profiles || profiles.length === 0) return null;
|
||||
|
||||
const profileListItems = profiles.map((profile) => {
|
||||
return (
|
||||
<li key={profile.profile_id} className={`${baseClass}__profile`}>
|
||||
<div className={`${baseClass}__profile-data`}>
|
||||
<Icon name="profile" />
|
||||
<div className={`${baseClass}__profile-info`}>
|
||||
<span className={`${baseClass}__profile-name`}>
|
||||
{profile.name}
|
||||
</span>
|
||||
<span className={`${baseClass}__profile-uploaded`}>
|
||||
{`Uploaded ${formatDistanceToNow(
|
||||
new Date(profile.created_at)
|
||||
)} ago`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__profile-actions`}>
|
||||
<Button
|
||||
className={`${baseClass}__download-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onClickDownload(profile)}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__delete-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onClickDelete(profile)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`${baseClass}__profiles`}>
|
||||
<div className={`${baseClass}__profiles-header`}>
|
||||
<span>Configuration profile</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
<ul className={`${baseClass}__profile-list`}>{profileListItems}</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const onFileUpload = async (files: FileList | null) => {
|
||||
setShowLoading(true);
|
||||
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
if (
|
||||
file.type !== "application/x-apple-aspen-config" ||
|
||||
!file.name.includes(".mobileconfig")
|
||||
) {
|
||||
renderFlash("error", UPLOAD_ERROR_MESSAGES.wrongType.message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mdmAPI.uploadProfile(file, currentTeam?.id);
|
||||
refectchProfiles();
|
||||
renderFlash("success", "Successfully uploaded!");
|
||||
} catch (e) {
|
||||
const error = e as AxiosResponse<IApiError>;
|
||||
const errMessage = getErrorMessage(error);
|
||||
renderFlash("error", errMessage);
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onCancelDelete = () => {
|
||||
selectedProfile.current = null;
|
||||
setShowDeleteProfileModal(false);
|
||||
};
|
||||
|
||||
const onDeleteProfile = async (profileId: number) => {
|
||||
try {
|
||||
await mdmAPI.deleteProfile(profileId);
|
||||
refectchProfiles();
|
||||
renderFlash("success", "Successfully deleted!");
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn’t delete. Please try again.");
|
||||
} finally {
|
||||
selectedProfile.current = null;
|
||||
setShowDeleteProfileModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Custom Settings</h2>
|
||||
<p className={`${baseClass}__description`}>
|
||||
Create and upload configuration profiles to apply custom settings.{" "}
|
||||
<CustomLink
|
||||
newTab
|
||||
text="Learn how"
|
||||
url="https://fleetdm.com/docs/controls#macos-settings"
|
||||
/>
|
||||
</p>
|
||||
|
||||
{renderProfiles()}
|
||||
|
||||
<div className={`${baseClass}__profile-uploader`}>
|
||||
<Icon name="profile" />
|
||||
<p>Configuration profile (.mobileconfig)</p>
|
||||
<Button isLoading={showLoading}>
|
||||
<label htmlFor="upload-profile">Upload</label>
|
||||
</Button>
|
||||
<input
|
||||
accept=".mobileconfig,application/x-apple-aspen-config"
|
||||
id="upload-profile"
|
||||
type="file"
|
||||
onChange={(e) => onFileUpload(e.target.files)}
|
||||
/>
|
||||
</div>
|
||||
{showDeleteProfileModal && selectedProfile.current && (
|
||||
<DeleteProfileModal
|
||||
profileName={selectedProfile.current?.name}
|
||||
profileId={selectedProfile.current?.profile_id}
|
||||
onCancel={onCancelDelete}
|
||||
onDelete={onDeleteProfile}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomSettings;
|
@ -0,0 +1,81 @@
|
||||
.custom-settings {
|
||||
|
||||
h2 {
|
||||
padding-bottom: $pad-small;
|
||||
font-size: $medium;
|
||||
font-weight: $regular;
|
||||
color: $core-fleet-black;
|
||||
border-bottom: solid 1px $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__description {
|
||||
font-size: $x-small;
|
||||
margin: $pad-xxlarge 0;
|
||||
}
|
||||
|
||||
&__profiles-header {
|
||||
padding: $pad-medium $pad-large;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__profile-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__profile {
|
||||
padding: $pad-medium $pad-large;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $ui-fleet-black-10;
|
||||
}
|
||||
|
||||
&__profile-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
&__profile-info {
|
||||
margin-left: $pad-medium;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__profile-name {
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__profile-uploaded {
|
||||
font-size: $xx-small;
|
||||
}
|
||||
|
||||
&__download-button, &__delete-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&__download-button {
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
|
||||
&__profile-uploader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-radius: $border-radius;
|
||||
background-color: $ui-fleet-blue-10;
|
||||
border: 1px solid $ui-fleet-black-10;
|
||||
padding: $pad-xlarge $pad-large;
|
||||
font-size: $x-small;
|
||||
margin-top: $pad-xxlarge;
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
interface DeleteProfileModalProps {
|
||||
profileName: string;
|
||||
profileId: number;
|
||||
onCancel: () => void;
|
||||
onDelete: (profileId: number) => void;
|
||||
}
|
||||
|
||||
const baseClass = "delete-profile-modal";
|
||||
|
||||
const generateMessageSuffix = (isPremiumTier?: boolean, teamId?: number) => {
|
||||
if (!isPremiumTier) {
|
||||
return "";
|
||||
}
|
||||
return teamId ? " assigned to this team" : " with no team";
|
||||
};
|
||||
|
||||
const DeleteProfileModal = ({
|
||||
profileName,
|
||||
profileId,
|
||||
onCancel,
|
||||
onDelete,
|
||||
}: DeleteProfileModalProps) => {
|
||||
const { isPremiumTier, currentTeam } = useContext(AppContext);
|
||||
|
||||
const messageSuffix = generateMessageSuffix(isPremiumTier, currentTeam?.id);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={baseClass}
|
||||
title={"Delete configuration profile"}
|
||||
onExit={onCancel}
|
||||
onEnter={() => onDelete(profileId)}
|
||||
>
|
||||
<>
|
||||
<p>
|
||||
This action will delete configuration profile{" "}
|
||||
<span className={`${baseClass}__profile-name`}>{profileName}</span>{" "}
|
||||
from all macOS hosts{messageSuffix}.
|
||||
</p>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => onDelete(profileId)}
|
||||
variant="alert"
|
||||
className="delete-loading"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={onCancel} variant="inverse-alert">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteProfileModal;
|
@ -0,0 +1,5 @@
|
||||
.delete-profile-modal {
|
||||
&__profile-name {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { IApiError } from "interfaces/errors";
|
||||
|
||||
export const UPLOAD_ERROR_MESSAGES = {
|
||||
wrongType: {
|
||||
condition: () => false,
|
||||
message: "Couldn’t upload. The file should be a .mobileconfig file.",
|
||||
},
|
||||
identifierExists: {
|
||||
condition: (reason: string) =>
|
||||
reason.includes("MDMAppleConfigProfile.PayloadIdentifier"),
|
||||
message:
|
||||
"Couldn’t upload. A configuration profile with this identifier (PayloadIdentifier) already exists.",
|
||||
},
|
||||
nameExists: {
|
||||
condition: (reason: string) => reason.includes("PayloadDisplayName"),
|
||||
message:
|
||||
"Couldn’t upload. A configuration profile with this name (PayloadDisplayName) already exists.",
|
||||
},
|
||||
encrypted: {
|
||||
condition: (reason: string) => reason.includes("encrypted"),
|
||||
message: "Couldn’t upload. The file should be unencrypted.",
|
||||
},
|
||||
validXML: {
|
||||
condition: (reason: string) => reason.includes("parsing XML"),
|
||||
message: "Couldn’t upload. The file should include valid XML.",
|
||||
},
|
||||
fileVault: {
|
||||
condition: (reason: string) =>
|
||||
reason.includes("unsupported PayloadType(s): com.apple.MCX.FileVault2"),
|
||||
message:
|
||||
"Couldn’t upload. The configuration profile can’t include FileVault settings. To control these settings, go to Disk encryption.",
|
||||
},
|
||||
default: {
|
||||
condition: () => false,
|
||||
message: "Couldn’t upload. Please try again.",
|
||||
},
|
||||
};
|
||||
|
||||
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
|
||||
const apiReason = err.data.errors[0].reason;
|
||||
|
||||
const error = Object.values(UPLOAD_ERROR_MESSAGES).find((errType) =>
|
||||
errType.condition(apiReason)
|
||||
);
|
||||
|
||||
if (!error) {
|
||||
return UPLOAD_ERROR_MESSAGES.default.message;
|
||||
}
|
||||
|
||||
return error.message;
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { default } from "./CustomSettings";
|
1
frontend/pages/ManageControlsPage/MacOSSettings/index.ts
Normal file
1
frontend/pages/ManageControlsPage/MacOSSettings/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./MacOSSettings";
|
@ -48,6 +48,7 @@ import ManageControlsPage from "pages/ManageControlsPage/ManageControlsPage";
|
||||
import MembersPage from "pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage";
|
||||
import AgentOptionsPage from "pages/admin/TeamManagementPage/TeamDetailsWrapper/AgentOptionsPage";
|
||||
import MacOSUpdates from "pages/MacOSUpdates";
|
||||
import MacOSSettings from "pages/ManageControlsPage/MacOSSettings";
|
||||
|
||||
import PATHS from "router/paths";
|
||||
import AppProvider from "context/app";
|
||||
@ -79,17 +80,6 @@ const AppWrapper = ({ children, location }: IAppWrapperProps) => (
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
const MacSettingsPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<EmptyTable
|
||||
header="Coming soon"
|
||||
info="The ability to store disk encryption keys and customize macOS settings are currently in development."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const routes = (
|
||||
<Router history={browserHistory}>
|
||||
<Route path={PATHS.ROOT} component={AppWrapper}>
|
||||
@ -179,7 +169,8 @@ const routes = (
|
||||
<IndexRedirect to={"mac-os-updates"} />
|
||||
<Route component={ManageControlsPage}>
|
||||
<Route path="mac-os-updates" component={MacOSUpdates} />
|
||||
<Route path="mac-settings" component={MacSettingsPage} />
|
||||
<Route path="mac-settings" component={MacOSSettings} />
|
||||
<Route path="mac-settings/:section" component={MacOSSettings} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { IHost } from "../interfaces/host";
|
||||
import { IQuery } from "../interfaces/query";
|
||||
import { IPolicy } from "../interfaces/policy";
|
||||
import URL_PREFIX from "./url_prefix";
|
||||
@ -8,6 +7,7 @@ export default {
|
||||
CONTROLS: `${URL_PREFIX}/controls`,
|
||||
CONTROLS_MAC_OS_UPDATES: `${URL_PREFIX}/controls/mac-os-updates`,
|
||||
CONTROLS_MAC_SETTINGS: `${URL_PREFIX}/controls/mac-settings`,
|
||||
CONTROLS_CUSTOM_SETTINGS: `${URL_PREFIX}/controls/mac-settings/custom-settings`,
|
||||
DASHBOARD: `${URL_PREFIX}/dashboard`,
|
||||
DASHBOARD_LINUX: `${URL_PREFIX}/dashboard/linux`,
|
||||
DASHBOARD_MAC: `${URL_PREFIX}/dashboard/mac`,
|
||||
|
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import sendRequest from "services";
|
||||
import endpoints from "utilities/endpoints";
|
||||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
export default {
|
||||
downloadDeviceUserEnrollmentProfile: (token: string) => {
|
||||
@ -25,4 +26,39 @@ export default {
|
||||
organization,
|
||||
});
|
||||
},
|
||||
|
||||
getProfiles: (teamId?: number) => {
|
||||
const { MDM_PROFILES } = endpoints;
|
||||
|
||||
let path = MDM_PROFILES;
|
||||
|
||||
if (teamId) {
|
||||
path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`;
|
||||
}
|
||||
|
||||
return sendRequest("GET", path);
|
||||
},
|
||||
|
||||
uploadProfile: (file: File, teamId?: number) => {
|
||||
const { MDM_PROFILES } = endpoints;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("profile", file);
|
||||
|
||||
if (teamId) {
|
||||
formData.append("team_id", teamId.toString());
|
||||
}
|
||||
|
||||
return sendRequest("POST", MDM_PROFILES, formData);
|
||||
},
|
||||
|
||||
downloadProfile: (profileId: number) => {
|
||||
const { MDM_PROFILE } = endpoints;
|
||||
return sendRequest("GET", MDM_PROFILE(profileId));
|
||||
},
|
||||
|
||||
deleteProfile: (profileId: number) => {
|
||||
const { MDM_PROFILE } = endpoints;
|
||||
return sendRequest("DELETE", MDM_PROFILE(profileId));
|
||||
},
|
||||
};
|
||||
|
@ -34,12 +34,13 @@ export default {
|
||||
LOGIN: `/${API_VERSION}/fleet/login`,
|
||||
LOGOUT: `/${API_VERSION}/fleet/logout`,
|
||||
MACADMINS: `/${API_VERSION}/fleet/macadmins`,
|
||||
// TODO: Clean up MDM endpoints to be consistent and up to date
|
||||
MDM_APPLE: `/${API_VERSION}/fleet/mdm/apple`,
|
||||
MDM_APPLE_BM: `/${API_VERSION}/fleet/mdm/apple_bm`,
|
||||
MDM_APPLE_BM_KEYS: `/${API_VERSION}/fleet/mdm/apple/dep/key_pair`,
|
||||
MDM_SUMMARY: `/${API_VERSION}/fleet/hosts/summary/mdm`,
|
||||
MDM_REQUEST_CSR: `/${API_VERSION}/fleet/mdm/apple/request_csr`,
|
||||
MDM_PROFILES: `/${API_VERSION}/fleet/mdm/apple/profiles`,
|
||||
MDM_PROFILE: (id: number) => `/${API_VERSION}/fleet/mdm/apple/profiles/${id}`,
|
||||
// Should below 2 endpoints be consistent?
|
||||
HOST_MDM: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/mdm`,
|
||||
HOST_MDM_UNENROLL: (id: number) =>
|
||||
|
Loading…
Reference in New Issue
Block a user