Skip to content

Commit f044397

Browse files
committed
setup assistant UI
1 parent 73e5f47 commit f044397

File tree

22 files changed

+465
-6
lines changed

22 files changed

+465
-6
lines changed

frontend/components/Card/Card.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,20 @@ type CardColor = "white" | "gray" | "purple" | "yellow";
88

99
interface ICardProps {
1010
children?: React.ReactNode;
11-
/** The size of the border radius. Defaults to `small` */
11+
/** The size of the border radius. Defaults to `small`. */
1212
borderRadiusSize?: BorderRadiusSize;
1313
/** Includes the card shadows. Defaults to `false` */
1414
includeShadow?: boolean;
1515
/** The color of the card. Defaults to `white` */
1616
color?: CardColor;
1717
className?: string;
18-
/** Increases to 40px padding. Defaults to `false` */
18+
/** The size of the padding around the content of the card. Defaults to `large`.
19+
*
20+
* These correspond to the padding sizes in the design system. Look at `padding.scss` for values */
21+
paddingSize?: "small" | "medium" | "large" | "xlarge" | "xxlarge";
22+
/** NOTE: DEPRICATED. Use `paddingSize` prop instead.
23+
*
24+
* Increases to 40px padding. Defaults to `false` */
1925
largePadding?: boolean;
2026
}
2127

@@ -30,12 +36,16 @@ const Card = ({
3036
color = "white",
3137
className,
3238
largePadding = false,
39+
paddingSize = "large",
3340
}: ICardProps) => {
3441
const classNames = classnames(
3542
baseClass,
3643
`${baseClass}__${color}`,
3744
`${baseClass}__radius-${borderRadiusSize}`,
3845
{
46+
// TODO: simplify this when we've replaced largePadding prop with paddingSize
47+
[`${baseClass}__padding-${paddingSize}`]:
48+
!largePadding && paddingSize !== undefined,
3949
[`${baseClass}__shadow`]: includeShadow,
4050
[`${baseClass}__large-padding`]: largePadding,
4151
},

frontend/components/Card/_styles.scss

+22
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,29 @@
2222
box-shadow: $box-shadow;
2323
}
2424

25+
&__padding-small {
26+
padding: $pad-small;
27+
}
28+
29+
&__padding-medium {
30+
padding: $pad-medium;
31+
}
32+
33+
&__padding-large {
34+
padding: $pad-large;
35+
}
36+
37+
&__padding-xlarge {
38+
padding: $pad-xlarge;
39+
}
40+
41+
&__padding-xxlarge {
42+
padding: $pad-xxlarge;
43+
}
44+
2545
// 40px padding
46+
// TODO: remove when we've replaced all instances of largePadding with
47+
// paddingSize prop
2648
&__large-padding {
2749
padding: $pad-xxlarge;
2850
}

frontend/components/FileUploader/FileUploader.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ interface IFileUploaderProps {
3232
* https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
3333
*/
3434
accept?: string;
35+
/** The text to display on the upload button */
36+
buttonMessage?: string;
3537
className?: string;
3638
onFileUpload: (files: FileList | null) => void;
3739
}
@@ -45,6 +47,7 @@ const FileUploader = ({
4547
additionalInfo,
4648
isLoading = false,
4749
accept,
50+
buttonMessage = "Upload",
4851
className,
4952
onFileUpload,
5053
}: IFileUploaderProps) => {
@@ -73,7 +76,7 @@ const FileUploader = ({
7376
variant="brand"
7477
isLoading={isLoading}
7578
>
76-
<label htmlFor="upload-profile">Upload</label>
79+
<label htmlFor="upload-profile">{buttonMessage}</label>
7780
</Button>
7881
<input
7982
accept={accept}

frontend/components/FileUploader/_styles.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@
3030

3131
&__upload-button {
3232
margin-top: 8px;
33+
// we handle the padding in the label so the entire button is clickable
3334
padding: 0;
3435
}
3536

3637
label {
37-
height: 36px;
38-
width: 78.2667px;
38+
padding: $pad-small $pad-medium;
3939
display: flex;
4040
align-items: center;
4141
justify-content: center;

frontend/pages/ManageControlsPage/SetupExperience/SetupExperienceNavItems.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ISideNavItem } from "pages/admin/components/SideNav/SideNav";
44

55
import EndUserAuthentication from "./cards/EndUserAuthentication/EndUserAuthentication";
66
import BootstrapPackage from "./cards/BootstrapPackage";
7+
import SetupAssistant from "./cards/SetupAssistant";
78

89
interface ISetupExperienceCardProps {
910
currentTeamId?: number;
@@ -25,6 +26,12 @@ const SETUP_EXPERIENCE_NAV_ITEMS: ISideNavItem<
2526
path: PATHS.CONTROLS_BOOTSTRAP_PACKAGE,
2627
Card: BootstrapPackage,
2728
},
29+
{
30+
title: "Setup assistant",
31+
urlSection: "setup-assistant",
32+
path: PATHS.CONTROLS_SETUP_ASSITANT,
33+
Card: SetupAssistant,
34+
},
2835
];
2936

3037
export default SETUP_EXPERIENCE_NAV_ITEMS;

frontend/pages/ManageControlsPage/SetupExperience/cards/BootstrapPackage/components/BootstrapPackagePreview/_styles.scss

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
font-size: $x-small;
77

88
h2 {
9-
font-size: $x-small; // needed to override global h2 style
9+
margin: 0;
10+
font-size: $small;
11+
font-weight: normal;
1012
}
1113

1214
&__preview-img {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React, { useState } from "react";
2+
3+
import SectionHeader from "components/SectionHeader";
4+
import Spinner from "components/Spinner";
5+
6+
import SetupAssistantPreview from "./components/SetupAssistantPreview";
7+
import SetupAssistantPackageUploader from "./components/SetupAssistantPackageUploader";
8+
import SetuAssistantUploadedProfileView from "./components/SetupAssistantUploadedProfileView/SetupAssistantUploadedProfileView";
9+
10+
const baseClass = "setup-assistant";
11+
12+
interface ISetupAssistantProps {
13+
currentTeamId: number;
14+
}
15+
16+
const StartupAssistant = ({ currentTeamId }: ISetupAssistantProps) => {
17+
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
18+
19+
const isLoading = false;
20+
21+
const noPackageUploaded = true;
22+
23+
return (
24+
<div className={baseClass}>
25+
<SectionHeader title="Setup assistant" />
26+
{isLoading ? (
27+
<Spinner />
28+
) : (
29+
<div className={`${baseClass}__content`}>
30+
{false ? (
31+
<>
32+
<SetupAssistantPackageUploader
33+
currentTeamId={currentTeamId}
34+
onUpload={() => 1}
35+
/>
36+
<div className={`${baseClass}__preview-container`}>
37+
<SetupAssistantPreview />
38+
</div>
39+
</>
40+
) : (
41+
<>
42+
<SetuAssistantUploadedProfileView
43+
profileMetaData={1}
44+
currentTeamId={currentTeamId}
45+
onDelete={() => setShowDeleteProfileModal(true)}
46+
/>
47+
<div className={`${baseClass}__preview-container`}>
48+
<SetupAssistantPreview />
49+
</div>
50+
</>
51+
)}
52+
</div>
53+
)}
54+
{/* {showDeleteProfileModal && (
55+
<DeletePackageModal
56+
onDelete={onDelete}
57+
onCancel={() => setShowDeletePackageModal(false)}
58+
/>
59+
)} */}
60+
</div>
61+
);
62+
};
63+
64+
export default StartupAssistant;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.setup-assistant {
2+
&__content {
3+
max-width: $break-xxl;
4+
margin: 0 auto;
5+
display: flex;
6+
justify-content: space-between;
7+
gap: $pad-xxlarge;
8+
}
9+
10+
@media (max-width: $break-md) {
11+
&__content {
12+
flex-direction: column;
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useContext, useState } from "react";
2+
import { AxiosResponse } from "axios";
3+
4+
import { IApiError } from "interfaces/errors";
5+
import { NotificationContext } from "context/notification";
6+
import mdmAPI from "services/entities/mdm";
7+
8+
import CustomLink from "components/CustomLink";
9+
import FileUploader from "components/FileUploader";
10+
11+
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
12+
13+
const baseClass = "setup-assistant-package-uploader";
14+
15+
interface ISetupAssistantPackageUploaderProps {
16+
currentTeamId: number;
17+
onUpload: () => void;
18+
}
19+
20+
const SetupAssistantPackageUploader = ({
21+
currentTeamId,
22+
onUpload,
23+
}: ISetupAssistantPackageUploaderProps) => {
24+
const { renderFlash } = useContext(NotificationContext);
25+
const [showLoading, setShowLoading] = useState(false);
26+
27+
const onUploadFile = async (files: FileList | null) => {
28+
setShowLoading(true);
29+
30+
if (!files || files.length === 0) {
31+
setShowLoading(false);
32+
return;
33+
}
34+
35+
const file = files[0];
36+
37+
// quick exit if the file type is incorrect
38+
if (!file.name.includes(".pkg")) {
39+
renderFlash("error", UPLOAD_ERROR_MESSAGES.wrongType.message);
40+
setShowLoading(false);
41+
return;
42+
}
43+
44+
try {
45+
await mdmAPI.uploadBootstrapPackage(file, currentTeamId);
46+
renderFlash("success", "Successfully uploaded!");
47+
onUpload();
48+
} catch (e) {
49+
const error = e as AxiosResponse<IApiError>;
50+
const errMessage = getErrorMessage(error);
51+
renderFlash("error", errMessage);
52+
} finally {
53+
setShowLoading(false);
54+
}
55+
};
56+
57+
return (
58+
<div className={baseClass}>
59+
<p>
60+
Add an automatic enrollment profile to customize the macOS Setup
61+
Assistant.
62+
<CustomLink
63+
url=" https://fleetdm.com/learn-more-about/setup-assistant"
64+
text="Learn how"
65+
newTab
66+
/>
67+
</p>
68+
<FileUploader
69+
message="Automatic enrollment profile (.json)"
70+
graphicName="file-configuration-profile"
71+
accept=".json"
72+
buttonMessage="Add profile"
73+
onFileUpload={onUploadFile}
74+
isLoading={showLoading}
75+
/>
76+
</div>
77+
);
78+
};
79+
80+
export default SetupAssistantPackageUploader;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.setup-assistant-package-uploader {
2+
> p {
3+
font-size: $x-small;
4+
margin: 0 0 $pad-large;
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { AxiosResponse } from "axios";
2+
import { IApiError } from "interfaces/errors";
3+
4+
export const UPLOAD_ERROR_MESSAGES = {
5+
wrongType: {
6+
condition: (reason: string) => reason.includes("invalid file type"),
7+
message: "Couldn’t upload. The file should be a package (.pkg).",
8+
},
9+
unsigned: {
10+
condition: (reason: string) => reason.includes("file is not"),
11+
message:
12+
"Couldn’t upload. The package must be signed. Click “Learn more” below to learn how to sign.",
13+
},
14+
default: {
15+
condition: () => false,
16+
message: "Couldn’t upload. Please try again.",
17+
},
18+
};
19+
20+
export const getErrorMessage = (err: AxiosResponse<IApiError>) => {
21+
const apiReason = err.data.errors[0].reason;
22+
23+
const error = Object.values(UPLOAD_ERROR_MESSAGES).find((errType) =>
24+
errType.condition(apiReason)
25+
);
26+
27+
if (!error) {
28+
return UPLOAD_ERROR_MESSAGES.default.message;
29+
}
30+
31+
return error.message;
32+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from "./SetupAssistantPackageUploader";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from "react";
2+
3+
import Card from "components/Card";
4+
5+
import OsSetupPreview from "../../../../../../../../assets/images/os-setup-preview.gif";
6+
7+
const baseClass = "setup-assistant-preview";
8+
9+
const SetupAssistantPreview = () => {
10+
return (
11+
<Card
12+
color="gray"
13+
borderRadiusSize="medium"
14+
paddingSize="xxlarge"
15+
className={baseClass}
16+
>
17+
<h2>End user experience</h2>
18+
<p>
19+
After the end user continues past the <b>Remote Management</b> screen,
20+
macOS Setup Assistant displays several screens by default.
21+
</p>
22+
<p>
23+
By adding an automatic enrollment profile you can customize which
24+
screens are displayed and more.
25+
</p>
26+
<img
27+
className={`${baseClass}__preview-img`}
28+
src={OsSetupPreview}
29+
alt="OS setup preview"
30+
/>
31+
</Card>
32+
);
33+
};
34+
35+
export default SetupAssistantPreview;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.setup-assistant-preview {
2+
font-size: $x-small;
3+
4+
h2 {
5+
margin: 0;
6+
font-size: $small;
7+
font-weight: normal;
8+
}
9+
10+
&__preview-img {
11+
margin-top: $pad-xxlarge;
12+
width: 100%;
13+
display: block;
14+
margin: 40px auto 0;
15+
}
16+
}

0 commit comments

Comments
 (0)