diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets index 6cde7b7e8d..3b0b77465e 100644 --- a/.vscode/typescriptreact.code-snippets +++ b/.vscode/typescriptreact.code-snippets @@ -22,7 +22,7 @@ "scope": "typescriptreact,javascriptreact", "prefix": "bc", "body": [ - "`\\${baseClass}__$0`" + "className={`\\${baseClass}__$0`}" ] } } diff --git a/frontend/components/icons/FileBash.tsx b/frontend/components/icons/FileBash.tsx new file mode 100644 index 0000000000..8ffff65e36 --- /dev/null +++ b/frontend/components/icons/FileBash.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +const FileBash = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default FileBash; diff --git a/frontend/components/icons/FileGeneric.tsx b/frontend/components/icons/FileGeneric.tsx new file mode 100644 index 0000000000..c038867e44 --- /dev/null +++ b/frontend/components/icons/FileGeneric.tsx @@ -0,0 +1,68 @@ +import React from "react"; + +const FileGeneric = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default FileGeneric; diff --git a/frontend/components/icons/FilePython.tsx b/frontend/components/icons/FilePython.tsx new file mode 100644 index 0000000000..b7e00f0291 --- /dev/null +++ b/frontend/components/icons/FilePython.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +const FilePython = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default FilePython; diff --git a/frontend/components/icons/FileZsh.tsx b/frontend/components/icons/FileZsh.tsx new file mode 100644 index 0000000000..aee2fe94ad --- /dev/null +++ b/frontend/components/icons/FileZsh.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +const FileZsh = () => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default FileZsh; diff --git a/frontend/components/icons/Files.tsx b/frontend/components/icons/Files.tsx new file mode 100644 index 0000000000..61a1522ce5 --- /dev/null +++ b/frontend/components/icons/Files.tsx @@ -0,0 +1,203 @@ +import React from "react"; + +const Files = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default Files; diff --git a/frontend/components/icons/Refresh.tsx b/frontend/components/icons/Refresh.tsx new file mode 100644 index 0000000000..b402beb0b4 --- /dev/null +++ b/frontend/components/icons/Refresh.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +const Refresh = () => { + return ( + + + + ); +}; + +export default Refresh; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index 7329147edc..5644b9cc42 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -42,6 +42,12 @@ import Pencil from "./Pencil"; import TrashCan from "./TrashCan"; import Profile from "./Profile"; import Download from "./Download"; +import Files from "./Files"; +import Refresh from "./Refresh"; +import FilePython from "./FilePython"; +import FileZsh from "./FileZsh"; +import FileBash from "./FileBash"; +import FileGeneric from "./FileGeneric"; // a mapping of the usable names of icons to the icon source. export const ICON_MAP = { @@ -86,6 +92,12 @@ export const ICON_MAP = { "linux-green": LinuxGreen, profile: Profile, download: Download, + files: Files, + "file-python": FilePython, + "file-zsh": FileZsh, + "file-bash": FileBash, + "file-generic": FileGeneric, + refresh: Refresh, }; export type IconNames = keyof typeof ICON_MAP; diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index cbc3edd926..fcc546024e 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -71,12 +71,23 @@ export interface IMdmProfilesResponse { export type MacMdmProfileStatus = "applied" | "pending" | "failed"; export type MacMdmProfileOperationType = "remove" | "install"; -export type IHostMacMdmProfile = { +export interface IHostMacMdmProfile { profile_id: number; name: string; operation_type: MacMdmProfileOperationType; status: MacMdmProfileStatus; detail: string; -}; +} export type IMacSettings = IHostMacMdmProfile[]; export type MacSettingsStatus = "Failing" | "Latest" | "Pending"; + +// TODO: update when we have API +export interface IMdmScript { + id: number; + name: string; + ran: number; + pending: number; + errors: number; + created_at: string; + updated_at: string; +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/MacOSScripts.tsx b/frontend/pages/ManageControlsPage/MacOSScripts/MacOSScripts.tsx new file mode 100644 index 0000000000..5ce01809b7 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/MacOSScripts.tsx @@ -0,0 +1,111 @@ +import React, { useRef, useState } from "react"; + +import { IMdmScript } from "interfaces/mdm"; + +import CustomLink from "components/CustomLink"; + +import ScriptListHeading from "./components/ScriptListHeading"; +import ScriptListItem from "./components/ScriptListItem"; +import DeleteScriptModal from "./components/DeleteScriptModal"; +import FileUploader from "../components/FileUploader"; +import UploadList from "../components/UploadList"; +import RerunScriptModal from "./components/RerunScriptModal"; + +// TODO: remove when get integrate with API. +const scripts = [ + { + id: 1, + name: "Test.py", + ran: 57, + pending: 2304, + errors: 0, + created_at: new Date().toString(), + }, +]; + +const baseClass = "mac-os-scripts"; + +const MacOSScripts = () => { + const [showRerunScriptModal, setShowRerunScriptModal] = useState(false); + const [showDeleteScriptModal, setShowDeleteScriptModal] = useState(false); + + const selectedScript = useRef(null); + + const onClickRerun = (script: IMdmScript) => { + selectedScript.current = script; + setShowRerunScriptModal(true); + }; + + const onClickDelete = (script: IMdmScript) => { + selectedScript.current = script; + setShowDeleteScriptModal(true); + }; + + const onCancelRerun = () => { + selectedScript.current = null; + setShowRerunScriptModal(false); + }; + + const onCancelDelete = () => { + selectedScript.current = null; + setShowDeleteScriptModal(false); + }; + + // TODO: change when integrating with API + const onRerunScript = (scriptId: number) => { + console.log("rerun", scriptId); + setShowRerunScriptModal(false); + }; + + // TODO: change when integrating with API + const onDeleteScript = (scriptId: number) => { + console.log("delete", scriptId); + setShowDeleteScriptModal(false); + }; + + return ( +
+

+ Upload scripts to change configuration and remediate issues on macOS + hosts. Each script runs once per host. All scripts can be rerun on end + users’ My device page. +

+ ( + + )} + /> + { + return null; + }} + /> + {showRerunScriptModal && selectedScript.current && ( + + )} + {showDeleteScriptModal && selectedScript.current && ( + + )} +
+ ); +}; + +export default MacOSScripts; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/_styles.scss b/frontend/pages/ManageControlsPage/MacOSScripts/_styles.scss new file mode 100644 index 0000000000..f5fa9ef546 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/_styles.scss @@ -0,0 +1,7 @@ +.mac-os-scripts { + font-size: $x-small; + + &__description { + margin: $pad-xxlarge 0; + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/DeleteScriptModal.tsx b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/DeleteScriptModal.tsx new file mode 100644 index 0000000000..c0f5cef6d0 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/DeleteScriptModal.tsx @@ -0,0 +1,52 @@ +import React from "react"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; + +const baseClass = "delete-script-modal"; + +interface IDeleteScriptModalProps { + scriptName: string; + scriptId: number; + onCancel: () => void; + onDelete: (scriptId: number) => void; +} + +const DeleteScriptModal = ({ + scriptName, + scriptId, + onCancel, + onDelete, +}: IDeleteScriptModalProps) => { + return ( + onDelete(scriptId)} + > + <> +

+ This action will cancel script{" "} + {scriptName} from + running on macOS hosts on which the scrupt hasn't run yet. +

+
+ + +
+ +
+ ); +}; + +export default DeleteScriptModal; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/_styles.scss b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/_styles.scss new file mode 100644 index 0000000000..882315657a --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/_styles.scss @@ -0,0 +1,5 @@ +.delete-script-modal { + &__script-name { + font-weight: $bold; + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/index.ts b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/index.ts new file mode 100644 index 0000000000..6cc0bb99f6 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/DeleteScriptModal/index.ts @@ -0,0 +1 @@ +export { default } from "./DeleteScriptModal"; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/RerunScriptModal.tsx b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/RerunScriptModal.tsx new file mode 100644 index 0000000000..b8081dbc66 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/RerunScriptModal.tsx @@ -0,0 +1,61 @@ +import React, { useContext } from "react"; + +import { AppContext } from "context/app"; + +import Modal from "components/Modal"; +import Button from "components/buttons/Button"; + +const baseClass = "rerun-script-modal"; + +interface IRerunScriptModalProps { + scriptName: string; + scriptId: number; + onCancel: () => void; + onRerun: (scriptId: number) => void; +} + +const generateMessageSuffix = (isPremiumTier?: boolean, teamId?: number) => { + if (!isPremiumTier) { + return ""; + } + return teamId ? " assigned to this team" : " with no team"; +}; + +const RerunScriptModal = ({ + scriptName, + scriptId, + onCancel, + onRerun, +}: IRerunScriptModalProps) => { + const { isPremiumTier, currentTeam } = useContext(AppContext); + + const messageSuffix = generateMessageSuffix(isPremiumTier, currentTeam?.id); + + return ( + onRerun(scriptId)} + > + <> +

+ This action will rerun script{" "} + {scriptName} on + all macOS hosts {messageSuffix}. +

+

This may cause the script to run more than once on some hosts.

+
+ + +
+ +
+ ); +}; + +export default RerunScriptModal; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/_styles.scss b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/_styles.scss new file mode 100644 index 0000000000..abb85d176e --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/_styles.scss @@ -0,0 +1,10 @@ +.rerun-script-modal { + p { + margin-top: 0; + margin-bottom: $pad-xlarge; + } + + &__script-name { + font-weight: $bold; + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/index.ts b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/index.ts new file mode 100644 index 0000000000..9032eb9c6b --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/RerunScriptModal/index.ts @@ -0,0 +1 @@ +export { default } from "./RerunScriptModal"; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/ScriptListHeading.tsx b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/ScriptListHeading.tsx new file mode 100644 index 0000000000..8958f7e03a --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/ScriptListHeading.tsx @@ -0,0 +1,78 @@ +import React from "react"; +import ReactTooltip from "react-tooltip"; + +import Icon from "components/Icon"; +import { COLORS } from "styles/var/colors"; + +const baseClass = "script-list-heading"; + +const ScriptListHeading = () => { + return ( +
+
+ Script +
+
+
+
+ + Ran +
+
+
+
+ + Pending +
+
+
+
+ + Errors +
+
+
+
+ Actions +
+ + + + Script ran and exited with status code 0. + + + + + Script will run when the host comes online. + + + + + Script ran and exited with a non-zero status code. Click on a host to + view error(s). + + +
+ ); +}; + +export default ScriptListHeading; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/_styles.scss b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/_styles.scss new file mode 100644 index 0000000000..19c0502075 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/_styles.scss @@ -0,0 +1,47 @@ +.script-list-heading { + display: flex; + justify-content: space-between; + + &__heading-group { + flex: 1; + } + + &__script-statuses { + display: flex; + justify-content: center; + } + + &__actions-heading { + text-align: right; + + span { + margin-right: 95px; // align with left side of buttons below it + } + } + + &__status > div { + display: flex; + align-items: center; + width: 100px; + justify-content: center; + + span { + margin-left: 12px; + } + } + + &__tooltip-text { + font-weight: normal; + } + + @media (max-width: $break-990) { + &__script-statuses { + justify-content: flex-end; + } + + + &__actions-heading { + display: none; + } + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/index.ts b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/index.ts new file mode 100644 index 0000000000..96bbba3ba4 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListHeading/index.ts @@ -0,0 +1 @@ +export { default } from "./ScriptListHeading"; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/ScriptListItem.tsx b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/ScriptListItem.tsx new file mode 100644 index 0000000000..2928de0259 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/ScriptListItem.tsx @@ -0,0 +1,95 @@ +import React from "react"; +import { formatDistanceToNow } from "date-fns"; + +import { IMdmScript } from "interfaces/mdm"; + +import Icon from "components/Icon"; +import Button from "components/buttons/Button"; + +const baseClass = "script-list-item"; + +interface IScriptListItemProps { + script: IMdmScript; + onRerun: (script: IMdmScript) => void; + onDelete: (script: IMdmScript) => void; +} + +const getStatusClassName = (value: number) => { + return value !== 0 ? `${baseClass}__has-value` : ""; +}; + +const getFileIconName = (fileName: string) => { + const fileExtension = fileName.split(".").pop(); + + switch (fileExtension) { + case "py": + return "file-python"; + case "zsh": + return "file-zsh"; + case "sh": + return "file-bash"; + default: + return "file-generic"; + } +}; + +const ScriptListItem = ({ + script, + onRerun, + onDelete, +}: IScriptListItemProps) => { + const onClickDownload = () => { + console.log("download"); + }; + + return ( +
+
+ +
+ {script.name} + + {`Uploaded ${formatDistanceToNow(new Date(script.created_at))} ago`} + +
+
+
+ {script.ran} + + {script.pending} + + + {script.errors} + +
+ +
+ + + +
+
+ ); +}; + +export default ScriptListItem; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/_styles.scss b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/_styles.scss new file mode 100644 index 0000000000..fb38997ef7 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/_styles.scss @@ -0,0 +1,69 @@ +.script-list-item { + display: flex; + align-items: center; + + &__value-group { + flex: 1; + } + + &__script-data { + display: flex; + align-items: center; + } + + &__script-info { + margin-left: $pad-medium; + display: flex; + flex-direction: column; + } + + &__script-name { + font-size: $x-small; + } + + &__script-uploaded { + font-size: $xx-small; + } + + &__script-statuses { + display: flex; + justify-content: center; + + span { + width: 100px; + text-align: center; + } + } + + + &__has-value { + color: $core-vibrant-blue + } + + &__script-actions { + display: flex; + justify-content: flex-end; + } + + &__refresh-button, + &__download-button, + &__delete-button { + width: 40px; + height: 40px; + } + + &__refresh-button, + &__download-button { + margin-right: $pad-medium; + } + + @media (max-width: $break-990) { + &__script-statuses { + justify-content: flex-end; + } + + &__script-actions { + display: none; + } + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/index.ts b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/index.ts new file mode 100644 index 0000000000..391ca650f1 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/components/ScriptListItem/index.ts @@ -0,0 +1 @@ +export { default } from "./ScriptListItem"; diff --git a/frontend/pages/ManageControlsPage/MacOSScripts/index.ts b/frontend/pages/ManageControlsPage/MacOSScripts/index.ts new file mode 100644 index 0000000000..5e3ff82cb5 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSScripts/index.ts @@ -0,0 +1 @@ +export { default } from "./MacOSScripts"; diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/CustomSettings.tsx b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/CustomSettings.tsx index 95433bcbea..d01aaf7299 100644 --- a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/CustomSettings.tsx +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/CustomSettings.tsx @@ -1,9 +1,6 @@ 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"; @@ -12,11 +9,14 @@ 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 FileUploader from "../../../components/FileUploader"; +import UploadList from "../../../components/UploadList"; import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers"; import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal"; +import ProfileListItem from "./components/ProfileListItem"; +import ProfileListHeading from "./components/ProfileListHeading"; const baseClass = "custom-settings"; @@ -42,69 +42,11 @@ const CustomSettings = () => { } ); - 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 ( -
  • -
    - -
    - - {profile.name} - - - {`Uploaded ${formatDistanceToNow( - new Date(profile.created_at) - )} ago`} - -
    -
    -
    - - -
    -
  • - ); - }); - - return ( -
    -
    - Configuration profile - Actions -
    - -
    - ); - }; - const onFileUpload = async (files: FileList | null) => { setShowLoading(true); @@ -163,21 +105,22 @@ const CustomSettings = () => { />

    - {renderProfiles()} - -
    - -

    Configuration profile (.mobileconfig)

    - - onFileUpload(e.target.files)} + {profiles && ( + ( + + )} /> -
    + )} + + {showDeleteProfileModal && selectedProfile.current && ( { + return ( +
    + Configuration profile + Actions +
    + ); +}; + +export default ProfileListHeading; diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/_styles.scss b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/_styles.scss new file mode 100644 index 0000000000..7fba306038 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/_styles.scss @@ -0,0 +1,11 @@ +.profile-list-heading { + display: flex; + justify-content: space-between; + font-size: $x-small; + font-weight: $bold; + + &__actions-heading { + text-align: right; + margin-right: 40px; // align with left side of buttons below it + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/index.ts b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/index.ts new file mode 100644 index 0000000000..9d202fbf6f --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListHeading/index.ts @@ -0,0 +1 @@ +export { default } from "./ProfileListHeading"; diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx new file mode 100644 index 0000000000..be514e0ae4 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/ProfileListItem.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { format, formatDistanceToNow } from "date-fns"; +import FileSaver from "file-saver"; + +import { IMdmProfile } from "interfaces/mdm"; +import mdmAPI from "services/entities/mdm"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; + +const baseClass = "profile-list-item"; + +interface IProfileListItemProps { + profile: IMdmProfile; + onDelete: (profile: IMdmProfile) => void; +} + +const ProfileListItem = ({ profile, onDelete }: IProfileListItemProps) => { + const onClickDownload = async () => { + 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); + }; + + return ( +
    +
    + +
    + {profile.name} + + {`Uploaded ${formatDistanceToNow( + new Date(profile.created_at) + )} ago`} + +
    +
    +
    + + +
    +
    + ); +}; + +export default ProfileListItem; diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/_styles.scss b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/_styles.scss new file mode 100644 index 0000000000..412b2d7787 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/_styles.scss @@ -0,0 +1,34 @@ +.profile-list-item { + display: flex; + justify-content: space-between; + align-items: center; + + &__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; + } +} diff --git a/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/index.ts b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/index.ts new file mode 100644 index 0000000000..121ec4b679 --- /dev/null +++ b/frontend/pages/ManageControlsPage/MacOSSettings/cards/CustomSettings/components/ProfileListItem/index.ts @@ -0,0 +1 @@ +export { default } from "./ProfileListItem"; diff --git a/frontend/pages/ManageControlsPage/components/FileUploader/FileUploader.tsx b/frontend/pages/ManageControlsPage/components/FileUploader/FileUploader.tsx new file mode 100644 index 0000000000..57c477037b --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/FileUploader/FileUploader.tsx @@ -0,0 +1,39 @@ +import React from "react"; + +import Button from "components/buttons/Button"; +import Icon from "components/Icon"; +import { IconNames } from "components/icons"; + +const baseClass = "file-uploader"; + +interface IFileUploaderProps { + icon: IconNames; + message: string; + isLoading?: boolean; + onFileUpload: (files: FileList | null) => void; +} + +const FileUploader = ({ + icon, + message, + isLoading = false, + onFileUpload, +}: IFileUploaderProps) => { + return ( +
    + +

    {message}

    + + onFileUpload(e.target.files)} + /> +
    + ); +}; + +export default FileUploader; diff --git a/frontend/pages/ManageControlsPage/components/FileUploader/_styles.scss b/frontend/pages/ManageControlsPage/components/FileUploader/_styles.scss new file mode 100644 index 0000000000..ecb40a0667 --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/FileUploader/_styles.scss @@ -0,0 +1,16 @@ +.file-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; + text-align: center; + + input { + display: none; + } +} diff --git a/frontend/pages/ManageControlsPage/components/FileUploader/index.ts b/frontend/pages/ManageControlsPage/components/FileUploader/index.ts new file mode 100644 index 0000000000..fd97e9686f --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/FileUploader/index.ts @@ -0,0 +1 @@ +export { default } from "./FileUploader"; diff --git a/frontend/pages/ManageControlsPage/components/UploadList/UploadList.tsx b/frontend/pages/ManageControlsPage/components/UploadList/UploadList.tsx new file mode 100644 index 0000000000..c3bd68a2ae --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/UploadList/UploadList.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +const baseClass = "upload-list"; + +interface IUploadListProps { + listItems: any[]; // TODO: typings + HeadingComponent: (props: any) => JSX.Element; // TODO: Typings + ListItemComponent: (props: { listItem: any }) => JSX.Element; // TODO: types +} + +const UploadList = ({ + listItems, + HeadingComponent, + ListItemComponent, +}: IUploadListProps) => { + const items = listItems.map((listItem) => { + return ( +
  • + +
  • + ); + }); + + return ( +
    +
    + +
    +
      {items}
    +
    + ); +}; + +export default UploadList; diff --git a/frontend/pages/ManageControlsPage/components/UploadList/_styles.scss b/frontend/pages/ManageControlsPage/components/UploadList/_styles.scss new file mode 100644 index 0000000000..fffbda697e --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/UploadList/_styles.scss @@ -0,0 +1,19 @@ +.upload-list { + &__header { + padding: $pad-medium $pad-large; + font-size: $x-small; + font-weight: $bold; + border-bottom: 1px solid $ui-fleet-black-10; + } + + &__list { + list-style: none; + padding: 0; + margin: 0; + } + + &__list-item { + padding: $pad-medium $pad-large; + border-bottom: 1px solid $ui-fleet-black-10; + } +} diff --git a/frontend/pages/ManageControlsPage/components/UploadList/index.ts b/frontend/pages/ManageControlsPage/components/UploadList/index.ts new file mode 100644 index 0000000000..2711bc749a --- /dev/null +++ b/frontend/pages/ManageControlsPage/components/UploadList/index.ts @@ -0,0 +1 @@ +export { default } from "./UploadList"; diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts index e4edcfc6cc..fdde2f05e8 100644 --- a/frontend/router/paths.ts +++ b/frontend/router/paths.ts @@ -8,6 +8,7 @@ export default { 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`, + CONTROLS_MAC_SCRIPTS: `${URL_PREFIX}/controls/mac-scripts`, DASHBOARD: `${URL_PREFIX}/dashboard`, DASHBOARD_LINUX: `${URL_PREFIX}/dashboard/linux`, DASHBOARD_MAC: `${URL_PREFIX}/dashboard/mac`,