implement mdm scripts page UI (#10092)

relates to #9831

Implements the mdm mac OS scripts UI. This is just the UI atm and is not
accessible in the application at the moment.
This commit is contained in:
Gabriel Hernandez 2023-03-06 15:03:48 +00:00 committed by GitHub
parent 50a2739609
commit b8fa08b53c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1276 additions and 131 deletions

View File

@ -22,7 +22,7 @@
"scope": "typescriptreact,javascriptreact", "scope": "typescriptreact,javascriptreact",
"prefix": "bc", "prefix": "bc",
"body": [ "body": [
"`\\${baseClass}__$0`" "className={`\\${baseClass}__$0`}"
] ]
} }
} }

View File

@ -0,0 +1,57 @@
import React from "react";
const FileBash = () => {
return (
<svg width="34" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#a)">
<path
d="M29.333 39.751H4.667a2.417 2.417 0 0 1-2.417-2.417V2.668A2.417 2.417 0 0 1 4.667.25h19.562c.64 0 1.255.255 1.709.708l5.104 5.104c.453.454.708 1.068.708 1.71v29.561a2.417 2.417 0 0 1-2.417 2.417Z"
fill="#fff"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M23.5.501h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M24.5.334v5.667c0 .737.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M2.5 20h19a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-19V20Z"
fill="#C5C7D1"
/>
<rect
x=".25"
y="18.584"
width="21.02"
height="16.351"
rx="1.75"
fill="#515774"
/>
<path
d="m10.004 26.514-1.913.052a.714.714 0 0 0-.162-.363.87.87 0 0 0-.345-.251 1.176 1.176 0 0 0-.486-.094c-.241 0-.447.048-.618.145-.168.097-.25.227-.247.392a.419.419 0 0 0 .153.332c.108.094.3.17.576.226l1.261.239c.653.125 1.14.332 1.457.622.321.29.483.673.486 1.15-.003.45-.136.84-.4 1.173-.262.332-.62.59-1.074.775-.455.182-.975.273-1.56.273-.935 0-1.672-.192-2.212-.575A2.149 2.149 0 0 1 4 29.058l2.058-.05c.046.238.164.42.354.545.19.125.433.187.729.187.267 0 .484-.05.652-.149.167-.1.252-.231.255-.396a.42.42 0 0 0-.196-.354c-.128-.09-.328-.162-.6-.213l-1.143-.217c-.656-.12-1.145-.34-1.466-.66-.32-.325-.48-.737-.477-1.237-.003-.437.114-.81.35-1.12.235-.313.57-.551 1.005-.716.435-.165.948-.247 1.539-.247.886 0 1.585.186 2.096.558.512.37.794.878.848 1.525ZM13.276 27.328v3.733h-2.084v-8.727h2.016v3.384h.072c.148-.404.39-.72.725-.947.338-.227.751-.34 1.24-.34.46 0 .86.102 1.201.306.341.202.605.488.793.857.19.37.284.801.281 1.295v4.172h-2.084V27.3c.003-.364-.088-.648-.272-.853-.185-.204-.445-.307-.78-.307a1.14 1.14 0 0 0-.58.145.988.988 0 0 0-.388.405c-.09.176-.137.39-.14.64Z"
fill="#fff"
/>
<rect
x=".25"
y="18.584"
width="21.02"
height="16.351"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
</defs>
</svg>
);
};
export default FileBash;

View File

@ -0,0 +1,68 @@
import React from "react";
const FileGeneric = () => {
return (
<svg width="34" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#a)">
<path
d="M29.333 39.751H4.667a2.417 2.417 0 0 1-2.417-2.417V2.668A2.417 2.417 0 0 1 4.667.25h19.562c.64 0 1.255.255 1.709.708l5.104 5.104c.453.454.708 1.068.708 1.71v29.561a2.417 2.417 0 0 1-2.417 2.417Z"
fill="#fff"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M23.5.501h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M24.5.334v5.667c0 .737.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<g clipPath="url(#b)">
<path
d="M15.113 26.751c.829.806 1.549 1.507 2.018 1.952.545.52.85 1.242.818 1.984-.033.743-.447 1.624-1.909 1.953-2.924.647-3.218-2.154-3.218-2.154l-.087-.817 2.389-2.918h-.011Z"
fill="#070708"
/>
<path
d="m8.916 17.298 8.302-7.714s2.073-.891 3.142.308c1.07 1.199.502 2.79.502 2.79l-7.506 7.3-4.44-2.684Z"
fill="#E2E4EA"
/>
<path
d="M15.113 26.751c-2.378-2.313-5.63-5.475-6.044-5.91-.665-.7-1.484-2.674.196-3.82a2.68 2.68 0 0 1 1.844-.413c2.716.371 2.236 3.374 2.236 3.374l5.902-5.698 5.608 5.496s1.418 1.507.632 2.525c-.785 1.019-7.952 9.518-7.952 9.518s1.003-1.305-.131-2.833c-.655-.891-2.302-2.239-2.302-2.239h.01Z"
fill="#C5C7D1"
/>
<path
d="M17.633 31.717c.84-1.008 7.112-8.456 7.854-9.411.797-1.019-.632-2.526-.632-2.526l-5.608-5.496 1.615-1.602s.567-1.591-.502-2.79c-1.07-1.199-3.142-.308-3.142-.308l-7.92 7.342-.382.372c-1.21 1.167-.458 2.896.142 3.544.415.434 3.666 3.596 6.044 5.91l-2.346 2.928.044.806s.294 2.812 3.218 2.154c.698-.16 1.156-.435 1.451-.764l.153-.16h.01Z"
stroke="#515774"
strokeWidth=".5"
strokeMiterlimit="10"
/>
<path
d="M15.113 26.751s1.898 1.655 2.269 2.196c.37.541 1.036 1.507.316 2.674"
stroke="#515774"
strokeWidth=".5"
strokeMiterlimit="10"
/>
<path
d="m19.345 14.189-6.021 5.835s.458-1.336-.557-2.578c-1.167-1.443-2.934-.764-2.934-.764a3.714 3.714 0 0 0-.666.372"
stroke="#515774"
strokeWidth=".5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
<clipPath id="b">
<path fill="#fff" d="M8 9h18v24H8z" />
</clipPath>
</defs>
</svg>
);
};
export default FileGeneric;

View File

@ -0,0 +1,57 @@
import React from "react";
const FilePython = () => {
return (
<svg width="34" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#a)">
<path
d="M29.333 39.751H4.667a2.417 2.417 0 0 1-2.417-2.417V2.668A2.417 2.417 0 0 1 4.667.25h19.562c.64 0 1.255.255 1.709.708l5.104 5.104c.453.454.708 1.068.708 1.71v29.561a2.417 2.417 0 0 1-2.417 2.417Z"
fill="#fff"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M23.5.501h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M24.5.334v5.667c0 .737.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M2.5 20h19a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-19V20Z"
fill="#C5C7D1"
/>
<rect
x=".25"
y="18.584"
width="21.314"
height="16.585"
rx="1.75"
fill="#5CABDF"
/>
<path
d="M4 31.42v-9h2.063v1.12h.063c.086-.199.206-.39.363-.575.159-.185.36-.336.605-.452a2.02 2.02 0 0 1 .886-.179c.455 0 .88.12 1.274.358.398.239.719.607.963 1.104.245.497.367 1.13.367 1.9 0 .742-.118 1.362-.354 1.862-.233.5-.548.875-.946 1.125-.395.25-.834.375-1.317.375-.33 0-.615-.054-.856-.162a1.864 1.864 0 0 1-.61-.426 2.046 2.046 0 0 1-.375-.566h-.042v3.515H4Zm2.041-5.728c0 .352.047.659.14.92.097.262.235.465.414.61.182.142.4.213.652.213.256 0 .473-.071.652-.213.18-.145.314-.348.405-.61.094-.261.14-.568.14-.92a2.69 2.69 0 0 0-.14-.916 1.29 1.29 0 0 0-.405-.601 1.005 1.005 0 0 0-.652-.213c-.256 0-.473.07-.652.209-.179.139-.317.338-.413.596-.094.259-.14.567-.14.925ZM12.739 31.42c-.25 0-.486-.02-.707-.06a2.863 2.863 0 0 1-.571-.15l.46-1.512c.204.068.389.108.554.119a.772.772 0 0 0 .43-.081.615.615 0 0 0 .281-.337l.081-.196-2.326-6.784h2.181l1.207 4.67h.068l1.223-4.67h2.194l-2.467 7.172c-.12.358-.288.673-.507.946a2.202 2.202 0 0 1-.84.648c-.34.156-.761.234-1.261.234Z"
fill="#fff"
/>
<rect
x=".25"
y="18.584"
width="21.314"
height="16.585"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
</defs>
</svg>
);
};
export default FilePython;

View File

@ -0,0 +1,57 @@
import React from "react";
const FileZsh = () => {
return (
<svg width="34" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#a)">
<path
d="M29.333 39.751H4.667a2.417 2.417 0 0 1-2.417-2.417V2.668A2.417 2.417 0 0 1 4.667.25h19.562c.64 0 1.255.255 1.709.708l5.104 5.104c.453.454.708 1.068.708 1.71v29.561a2.417 2.417 0 0 1-2.417 2.417Z"
fill="#fff"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M23.5.501h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M24.5.334v5.667c0 .737.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M2.5 20h25a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-25V20Z"
fill="#C5C7D1"
/>
<rect
x=".25"
y="18.581"
width="27.698"
height="16.351"
rx="1.75"
fill="#FAA669"
/>
<path
d="M4 31.058v-1.19l3.034-3.694v-.042H4.107v-1.62h5.411v1.304l-2.812 3.58v.043h2.915v1.619H4ZM16.682 26.511l-1.913.051a.713.713 0 0 0-.162-.362.87.87 0 0 0-.346-.252 1.176 1.176 0 0 0-.485-.093c-.242 0-.448.048-.618.145-.168.096-.25.227-.247.392a.419.419 0 0 0 .153.332c.108.094.3.17.575.226l1.262.239c.653.125 1.139.332 1.457.622.321.29.483.673.486 1.15-.003.45-.137.84-.4 1.172-.262.332-.62.591-1.075.776-.454.181-.974.272-1.56.272-.934 0-1.671-.191-2.211-.575a2.148 2.148 0 0 1-.92-1.551l2.058-.051c.045.238.163.42.353.545.19.125.434.188.73.188.266 0 .484-.05.651-.15.168-.099.253-.23.256-.396a.42.42 0 0 0-.196-.353c-.128-.091-.328-.162-.601-.213l-1.142-.218c-.656-.119-1.145-.34-1.466-.66-.321-.324-.48-.736-.477-1.236-.003-.438.113-.811.35-1.12.235-.313.57-.552 1.005-.717.434-.165.947-.247 1.538-.247.887 0 1.585.186 2.097.558.511.37.794.878.848 1.526ZM19.953 27.325v3.733H17.87V22.33h2.015v3.383h.073c.148-.403.389-.719.724-.946.338-.227.752-.34 1.24-.34.46 0 .861.101 1.202.306.34.202.605.487.793.856.19.37.284.802.28 1.296v4.172h-2.083v-3.763c.003-.364-.088-.648-.273-.852-.184-.205-.444-.307-.78-.307a1.14 1.14 0 0 0-.58.145.987.987 0 0 0-.387.405c-.09.176-.138.389-.14.639Z"
fill="#fff"
/>
<rect
x=".25"
y="18.581"
width="27.698"
height="16.351"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
</defs>
</svg>
);
};
export default FileZsh;

View File

@ -0,0 +1,203 @@
import React from "react";
const Files = () => {
return (
<svg width="142" 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"
/>
<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"
/>
<path
d="M2.5 20h19a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-19V20Z"
fill="#C5C7D1"
/>
<rect
x=".25"
y="18.584"
width="21.314"
height="16.585"
rx="1.75"
fill="#5CABDF"
/>
<path
d="M4 31.419v-9h2.063v1.12h.063c.086-.198.206-.39.363-.575.159-.184.36-.335.605-.451.247-.12.542-.18.886-.18.455 0 .88.12 1.274.359.398.238.719.606.963 1.103.245.497.367 1.13.367 1.9 0 .742-.118 1.363-.354 1.863-.233.5-.548.875-.946 1.125-.395.25-.834.375-1.317.375-.33 0-.615-.054-.856-.162a1.865 1.865 0 0 1-.61-.426 2.048 2.048 0 0 1-.375-.567h-.042v3.516H4Zm2.041-5.727c0 .352.047.659.14.92.097.261.235.464.414.61.182.141.4.212.652.212.256 0 .473-.07.652-.213.18-.145.314-.348.405-.61.094-.26.14-.567.14-.92 0-.352-.046-.657-.14-.916a1.29 1.29 0 0 0-.405-.6 1.006 1.006 0 0 0-.652-.214c-.256 0-.473.07-.652.21-.179.138-.317.337-.413.596-.094.258-.14.567-.14.925ZM12.739 31.419c-.25 0-.486-.02-.707-.06a2.863 2.863 0 0 1-.571-.15l.46-1.512c.204.068.389.108.554.12a.773.773 0 0 0 .43-.082.615.615 0 0 0 .281-.336l.081-.196-2.326-6.784h2.181l1.207 4.67h.068l1.223-4.67h2.194l-2.467 7.172c-.12.358-.288.673-.507.946a2.202 2.202 0 0 1-.84.647c-.34.157-.761.235-1.261.235Z"
fill="#fff"
/>
<rect
x=".25"
y="18.584"
width="21.314"
height="16.585"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<g clipPath="url(#b)">
<path
d="M65.333 39.75H40.667a2.417 2.417 0 0 1-2.417-2.416V2.667A2.417 2.417 0 0 1 40.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"
/>
<path
d="M59.5.5h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M60.5.334v5.667c0 .736.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M38.5 20h25a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-25V20Z"
fill="#C5C7D1"
/>
<rect
x="36.25"
y="18.58"
width="27.698"
height="16.351"
rx="1.75"
fill="#FAA669"
/>
<path
d="M40 31.057v-1.189l3.034-3.694v-.043h-2.928v-1.62h5.413v1.305l-2.813 3.58v.042h2.915v1.62H40ZM52.682 26.51l-1.913.052a.713.713 0 0 0-.163-.363.87.87 0 0 0-.345-.251 1.176 1.176 0 0 0-.485-.094c-.242 0-.448.049-.618.145-.168.097-.25.227-.248.392a.419.419 0 0 0 .154.333c.108.093.3.169.575.225l1.262.239c.653.125 1.139.332 1.457.622.321.29.483.674.486 1.15-.003.45-.137.84-.4 1.173-.262.332-.62.59-1.075.775-.454.182-.974.273-1.56.273-.934 0-1.671-.192-2.211-.575a2.148 2.148 0 0 1-.92-1.552l2.058-.05c.045.238.163.42.354.545.19.125.433.187.728.187.267 0 .485-.05.652-.149.168-.1.253-.231.256-.396a.42.42 0 0 0-.196-.354c-.128-.09-.328-.162-.601-.213l-1.142-.217c-.656-.12-1.145-.34-1.466-.66-.321-.325-.48-.736-.477-1.236-.003-.438.113-.812.35-1.121.235-.313.57-.551 1.005-.716.434-.165.947-.247 1.538-.247.887 0 1.585.186 2.097.558.511.37.794.878.848 1.526ZM55.953 27.324v3.733H53.87V22.33h2.015v3.384h.073c.148-.404.389-.72.724-.946.338-.228.752-.341 1.24-.341.46 0 .861.102 1.202.306.34.202.605.488.792.857.19.37.285.801.282 1.296v4.171h-2.084v-3.762c.003-.364-.088-.648-.273-.853-.184-.204-.444-.306-.78-.306a1.14 1.14 0 0 0-.58.144.988.988 0 0 0-.387.405c-.09.176-.138.39-.14.64Z"
fill="#fff"
/>
<rect
x="36.25"
y="18.58"
width="27.698"
height="16.351"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<g clipPath="url(#c)">
<path
d="M101.333 39.75H76.667a2.417 2.417 0 0 1-2.417-2.416V2.667A2.417 2.417 0 0 1 76.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"
/>
<path
d="M95.5.5h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M96.5.334v5.667c0 .736.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<path
d="M74.5 20h19a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-19V20Z"
fill="#C5C7D1"
/>
<rect
x="72.25"
y="18.584"
width="21.02"
height="16.351"
rx="1.75"
fill="#515774"
/>
<path
d="m82.004 26.514-1.913.051a.714.714 0 0 0-.162-.362.873.873 0 0 0-.345-.252 1.176 1.176 0 0 0-.486-.093c-.242 0-.447.048-.618.145-.168.096-.25.227-.247.392a.418.418 0 0 0 .153.332c.108.094.3.169.576.226l1.261.239c.653.125 1.14.332 1.457.622.321.29.483.673.486 1.15-.003.449-.136.84-.4 1.172-.262.332-.62.591-1.074.776-.455.181-.975.272-1.56.272-.935 0-1.672-.191-2.212-.575a2.148 2.148 0 0 1-.92-1.551l2.058-.051c.046.238.164.42.354.545.19.125.433.188.729.188.267 0 .484-.05.652-.15.167-.099.252-.23.255-.396a.42.42 0 0 0-.196-.353c-.128-.091-.328-.162-.6-.213l-1.143-.218c-.656-.12-1.144-.34-1.465-.66-.322-.324-.48-.736-.478-1.236-.003-.438.114-.811.35-1.12.235-.313.57-.552 1.005-.717.435-.165.948-.247 1.539-.247.886 0 1.585.186 2.096.558.512.37.794.878.848 1.526ZM85.276 27.328v3.733h-2.084v-8.727h2.016v3.383h.072c.148-.403.39-.719.725-.946.338-.227.751-.34 1.24-.34.46 0 .86.101 1.201.306.341.202.606.487.793.856.19.37.284.802.281 1.296v4.172h-2.084v-3.763c.003-.364-.088-.648-.272-.852-.185-.205-.445-.307-.78-.307a1.14 1.14 0 0 0-.58.145.988.988 0 0 0-.388.405c-.09.176-.137.389-.14.639Z"
fill="#fff"
/>
<rect
x="72.25"
y="18.584"
width="21.02"
height="16.351"
rx="1.75"
stroke="#192147"
strokeWidth=".5"
/>
</g>
<g clipPath="url(#d)">
<path
d="M137.333 39.75h-24.666a2.417 2.417 0 0 1-2.417-2.416V2.667A2.417 2.417 0 0 1 112.667.25h19.562c.641 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"
/>
<path
d="M131.5.5h.833l.5 6.5 6.667.5v1h-6a2 2 0 0 1-2-2v-6Z"
fill="#C5C7D1"
/>
<path
d="M132.5.334v5.667c0 .736.597 1.333 1.333 1.333h6"
stroke="#192147"
strokeWidth=".5"
/>
<g clipPath="url(#e)">
<path
d="M123.113 26.75c.829.807 1.549 1.507 2.018 1.953.545.52.851 1.241.818 1.984-.033.742-.447 1.623-1.909 1.952-2.924.647-3.218-2.154-3.218-2.154l-.087-.817 2.389-2.918h-.011Z"
fill="#8B8FA2"
/>
<path
d="m116.916 17.297 8.302-7.713s2.073-.892 3.142.307c1.069 1.2.502 2.79.502 2.79l-7.506 7.3-4.44-2.684Z"
fill="#E2E4EA"
/>
<path
d="M123.113 26.75c-2.378-2.313-5.629-5.474-6.044-5.91-.665-.7-1.484-2.673.196-3.819.535-.36 1.2-.499 1.844-.414 2.716.372 2.236 3.374 2.236 3.374l5.902-5.697 5.608 5.496s1.418 1.506.632 2.525c-.785 1.018-7.952 9.517-7.952 9.517s1.003-1.305-.131-2.833c-.655-.891-2.302-2.239-2.302-2.239h.011Z"
fill="#C5C7D1"
/>
<path
d="M125.633 31.716c.84-1.008 7.112-8.456 7.854-9.41.797-1.02-.633-2.526-.633-2.526l-5.607-5.496 1.615-1.602s.567-1.592-.502-2.79c-1.069-1.2-3.142-.308-3.142-.308l-7.92 7.342-.382.371c-1.211 1.167-.458 2.897.142 3.544.415.435 3.666 3.597 6.044 5.91l-2.346 2.928.044.807s.294 2.811 3.218 2.153c.698-.159 1.156-.435 1.451-.764l.153-.159h.011Z"
stroke="#515774"
strokeWidth=".5"
strokeMiterlimit="10"
/>
<path
d="M123.113 26.75s1.898 1.656 2.269 2.197c.371.54 1.036 1.506.316 2.674"
stroke="#515774"
strokeWidth=".5"
strokeMiterlimit="10"
/>
<path
d="m127.345 14.188-6.021 5.836s.458-1.337-.557-2.578c-1.167-1.443-2.934-.764-2.934-.764a3.691 3.691 0 0 0-.666.371"
stroke="#515774"
strokeWidth=".5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
<clipPath id="b">
<path fill="#fff" d="M36 0h34v40H36z" />
</clipPath>
<clipPath id="c">
<path fill="#fff" d="M72 0h34v40H72z" />
</clipPath>
<clipPath id="d">
<path fill="#fff" d="M108 0h34v40h-34z" />
</clipPath>
<clipPath id="e">
<path fill="#fff" d="M116 9h18v24h-18z" />
</clipPath>
</defs>
</svg>
);
};
export default Files;

View File

@ -0,0 +1,16 @@
import React from "react";
const Refresh = () => {
return (
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M13.996 1.004c0-.55.45-1 1-1s.999.45.999 1v3.998c0 .55-.45 1-1 1h-3.997c-.55 0-1-.45-1-1s.45-1 1-1h1.46A5.995 5.995 0 0 0 8 2.004 6.001 6.001 0 0 0 2.004 8c0 .55-.45 1-1 1s-1-.45-1-1A7.993 7.993 0 0 1 8 .005a7.945 7.945 0 0 1 5.996 2.738V1.004Zm0 6.996c0-.55.45-1 1-1s.999.45.999 1A7.993 7.993 0 0 1 8 15.995a7.945 7.945 0 0 1-5.996-2.738v1.739c0 .55-.45.999-1 .999s-1-.45-1-1v-3.997c0-.55.45-1 1-1h3.998c.55 0 1 .45 1 1s-.45 1-1 1h-1.46A5.995 5.995 0 0 0 8 13.995 6.001 6.001 0 0 0 13.996 8Z"
fill="#515774"
/>
</svg>
);
};
export default Refresh;

View File

@ -42,6 +42,12 @@ import Pencil from "./Pencil";
import TrashCan from "./TrashCan"; import TrashCan from "./TrashCan";
import Profile from "./Profile"; import Profile from "./Profile";
import Download from "./Download"; 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. // a mapping of the usable names of icons to the icon source.
export const ICON_MAP = { export const ICON_MAP = {
@ -86,6 +92,12 @@ export const ICON_MAP = {
"linux-green": LinuxGreen, "linux-green": LinuxGreen,
profile: Profile, profile: Profile,
download: Download, 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; export type IconNames = keyof typeof ICON_MAP;

View File

@ -71,12 +71,23 @@ export interface IMdmProfilesResponse {
export type MacMdmProfileStatus = "applied" | "pending" | "failed"; export type MacMdmProfileStatus = "applied" | "pending" | "failed";
export type MacMdmProfileOperationType = "remove" | "install"; export type MacMdmProfileOperationType = "remove" | "install";
export type IHostMacMdmProfile = { export interface IHostMacMdmProfile {
profile_id: number; profile_id: number;
name: string; name: string;
operation_type: MacMdmProfileOperationType; operation_type: MacMdmProfileOperationType;
status: MacMdmProfileStatus; status: MacMdmProfileStatus;
detail: string; detail: string;
}; }
export type IMacSettings = IHostMacMdmProfile[]; export type IMacSettings = IHostMacMdmProfile[];
export type MacSettingsStatus = "Failing" | "Latest" | "Pending"; 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;
}

View File

@ -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<IMdmScript | null>(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 (
<div className={baseClass}>
<p className={`${baseClass}__description`}>
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. <CustomLink text="Learn more" url="#" newTab />
</p>
<UploadList
listItems={scripts}
HeadingComponent={ScriptListHeading}
ListItemComponent={({ listItem }) => (
<ScriptListItem
script={listItem}
onRerun={onClickRerun}
onDelete={onClickDelete}
/>
)}
/>
<FileUploader
icon="files"
message="Any type of script supported by macOS. If you If you dont specify a shell or interpreter (e.g. #!/bin/sh), the script will run in /bin/sh."
onFileUpload={() => {
return null;
}}
/>
{showRerunScriptModal && selectedScript.current && (
<RerunScriptModal
scriptName={selectedScript.current?.name}
scriptId={selectedScript.current?.id}
onCancel={onCancelRerun}
onRerun={onRerunScript}
/>
)}
{showDeleteScriptModal && selectedScript.current && (
<DeleteScriptModal
scriptName={selectedScript.current?.name}
scriptId={selectedScript.current?.id}
onCancel={onCancelDelete}
onDelete={onDeleteScript}
/>
)}
</div>
);
};
export default MacOSScripts;

View File

@ -0,0 +1,7 @@
.mac-os-scripts {
font-size: $x-small;
&__description {
margin: $pad-xxlarge 0;
}
}

View File

@ -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 (
<Modal
className={baseClass}
title={"Delete script"}
onExit={onCancel}
onEnter={() => onDelete(scriptId)}
>
<>
<p>
This action will cancel script{" "}
<span className={`${baseClass}__script-name`}>{scriptName}</span> from
running on macOS hosts on which the scrupt hasn&apos;t run yet.
</p>
<div className="modal-cta-wrap">
<Button
type="button"
onClick={() => onDelete(scriptId)}
variant="alert"
className="delete-loading"
>
Delete
</Button>
<Button onClick={onCancel} variant="inverse-alert">
Cancel
</Button>
</div>
</>
</Modal>
);
};
export default DeleteScriptModal;

View File

@ -0,0 +1,5 @@
.delete-script-modal {
&__script-name {
font-weight: $bold;
}
}

View File

@ -0,0 +1 @@
export { default } from "./DeleteScriptModal";

View File

@ -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 (
<Modal
className={baseClass}
title={"Rerun Script"}
onExit={onCancel}
onEnter={() => onRerun(scriptId)}
>
<>
<p>
This action will rerun script{" "}
<span className={`${baseClass}__script-name`}>{scriptName}</span> on
all macOS hosts {messageSuffix}.
</p>
<p>This may cause the script to run more than once on some hosts.</p>
<div className="modal-cta-wrap">
<Button type="button" onClick={() => onRerun(scriptId)}>
Rerun
</Button>
<Button onClick={onCancel} variant="inverse">
Cancel
</Button>
</div>
</>
</Modal>
);
};
export default RerunScriptModal;

View File

@ -0,0 +1,10 @@
.rerun-script-modal {
p {
margin-top: 0;
margin-bottom: $pad-xlarge;
}
&__script-name {
font-weight: $bold;
}
}

View File

@ -0,0 +1 @@
export { default } from "./RerunScriptModal";

View File

@ -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 (
<div className={baseClass}>
<div className={`${baseClass}__heading-group`}>
<span>Script</span>
</div>
<div
className={`${baseClass}__heading-group ${baseClass}__script-statuses`}
>
<div className={`${baseClass}__status`}>
<div data-tip data-for="ran">
<Icon name="success" />
<span>Ran</span>
</div>
</div>
<div className={`${baseClass}__status`}>
<div data-tip data-for="pending">
<Icon name="pending" />
<span>Pending</span>
</div>
</div>
<div className={`${baseClass}__status`}>
<div data-tip data-for="errors">
<Icon name="error" />
<span>Errors</span>
</div>
</div>
</div>
<div
className={`${baseClass}__heading-group ${baseClass}__actions-heading`}
>
<span>Actions</span>
</div>
<ReactTooltip
type="dark"
effect="solid"
id="ran"
backgroundColor={COLORS["tooltip-bg"]}
>
<span className={`${baseClass}__tooltip-text`}>
Script ran and exited with status code 0.
</span>
</ReactTooltip>
<ReactTooltip
type="dark"
effect="solid"
id="pending"
backgroundColor={COLORS["tooltip-bg"]}
>
<span className={`${baseClass}__tooltip-text`}>
Script will run when the host comes online.
</span>
</ReactTooltip>
<ReactTooltip
type="dark"
effect="solid"
id="errors"
backgroundColor={COLORS["tooltip-bg"]}
>
<span className={`${baseClass}__tooltip-text`}>
Script ran and exited with a non-zero status code. Click on a host to
view error(s).
</span>
</ReactTooltip>
</div>
);
};
export default ScriptListHeading;

View File

@ -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;
}
}
}

View File

@ -0,0 +1 @@
export { default } from "./ScriptListHeading";

View File

@ -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 (
<div className={baseClass}>
<div className={`${baseClass}__value-group ${baseClass}__script-data`}>
<Icon name={getFileIconName(script.name)} />
<div className={`${baseClass}__script-info`}>
<span className={`${baseClass}__script-name`}>{script.name}</span>
<span className={`${baseClass}__script-uploaded`}>
{`Uploaded ${formatDistanceToNow(new Date(script.created_at))} ago`}
</span>
</div>
</div>
<div
className={`${baseClass}__value-group ${baseClass}__script-statuses`}
>
<span className={getStatusClassName(script.ran)}>{script.ran}</span>
<span className={getStatusClassName(script.pending)}>
{script.pending}
</span>
<span className={getStatusClassName(script.errors)}>
{script.errors}
</span>
</div>
<div className={`${baseClass}__value-group ${baseClass}__script-actions`}>
<Button
className={`${baseClass}__refresh-button`}
variant="text-icon"
onClick={() => onRerun(script)}
>
<Icon name="refresh" />
</Button>
<Button
className={`${baseClass}__download-button`}
variant="text-icon"
onClick={onClickDownload}
>
<Icon name="download" />
</Button>
<Button
className={`${baseClass}__delete-button`}
variant="text-icon"
onClick={() => onDelete(script)}
>
<Icon name="trash" color="ui-fleet-black-75" />
</Button>
</div>
</div>
);
};
export default ScriptListItem;

View File

@ -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;
}
}
}

View File

@ -0,0 +1 @@
export { default } from "./ScriptListItem";

View File

@ -0,0 +1 @@
export { default } from "./MacOSScripts";

View File

@ -1,9 +1,6 @@
import React, { useContext, useRef, useState } from "react"; import React, { useContext, useRef, useState } from "react";
import { useQuery } from "react-query"; import { useQuery } from "react-query";
import { AxiosResponse } from "axios"; 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 { IApiError } from "interfaces/errors";
import { IMdmProfile, IMdmProfilesResponse } from "interfaces/mdm"; import { IMdmProfile, IMdmProfilesResponse } from "interfaces/mdm";
@ -12,11 +9,14 @@ import { AppContext } from "context/app";
import { NotificationContext } from "context/notification"; import { NotificationContext } from "context/notification";
import CustomLink from "components/CustomLink"; 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 { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal"; import DeleteProfileModal from "./components/DeleteProfileModal/DeleteProfileModal";
import ProfileListItem from "./components/ProfileListItem";
import ProfileListHeading from "./components/ProfileListHeading";
const baseClass = "custom-settings"; 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) => { const onClickDelete = (profile: IMdmProfile) => {
selectedProfile.current = profile; selectedProfile.current = profile;
setShowDeleteProfileModal(true); 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) => { const onFileUpload = async (files: FileList | null) => {
setShowLoading(true); setShowLoading(true);
@ -163,21 +105,22 @@ const CustomSettings = () => {
/> />
</p> </p>
{renderProfiles()} {profiles && (
<UploadList
<div className={`${baseClass}__profile-uploader`}> listItems={profiles}
<Icon name="profile" /> HeadingComponent={ProfileListHeading}
<p>Configuration profile (.mobileconfig)</p> ListItemComponent={({ listItem }) => (
<Button isLoading={showLoading}> <ProfileListItem profile={listItem} onDelete={onClickDelete} />
<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> )}
<FileUploader
icon="profile"
message="Configuration profile (.mobileconfig)"
isLoading={showLoading}
onFileUpload={onFileUpload}
/>
{showDeleteProfileModal && selectedProfile.current && ( {showDeleteProfileModal && selectedProfile.current && (
<DeleteProfileModal <DeleteProfileModal
profileName={selectedProfile.current?.name} profileName={selectedProfile.current?.name}

View File

@ -28,55 +28,4 @@
padding: 0; padding: 0;
margin: 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;
}
}
} }

View File

@ -0,0 +1,14 @@
import React from "react";
const baseClass = "profile-list-heading";
const ProfileListHeading = () => {
return (
<div className={baseClass}>
<span>Configuration profile</span>
<span className={`${baseClass}__actions-heading`}>Actions</span>
</div>
);
};
export default ProfileListHeading;

View File

@ -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
}
}

View File

@ -0,0 +1 @@
export { default } from "./ProfileListHeading";

View File

@ -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 (
<div className={baseClass}>
<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}
>
<Icon name="download" />
</Button>
<Button
className={`${baseClass}__delete-button`}
variant="text-icon"
onClick={() => onDelete(profile)}
>
<Icon name="trash" color="ui-fleet-black-75" />
</Button>
</div>
</div>
);
};
export default ProfileListItem;

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { default } from "./ProfileListItem";

View File

@ -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 (
<div className={baseClass}>
<Icon name={icon} />
<p>{message}</p>
<Button isLoading={isLoading}>
<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>
);
};
export default FileUploader;

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { default } from "./FileUploader";

View File

@ -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 (
<li key={`${listItem.id}`} className={`${baseClass}__list-item`}>
<ListItemComponent listItem={listItem} />
</li>
);
});
return (
<div className={baseClass}>
<div className={`${baseClass}__header`}>
<HeadingComponent />
</div>
<ul className={`${baseClass}__list`}>{items}</ul>
</div>
);
};
export default UploadList;

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { default } from "./UploadList";

View File

@ -8,6 +8,7 @@ export default {
CONTROLS_MAC_OS_UPDATES: `${URL_PREFIX}/controls/mac-os-updates`, CONTROLS_MAC_OS_UPDATES: `${URL_PREFIX}/controls/mac-os-updates`,
CONTROLS_MAC_SETTINGS: `${URL_PREFIX}/controls/mac-settings`, CONTROLS_MAC_SETTINGS: `${URL_PREFIX}/controls/mac-settings`,
CONTROLS_CUSTOM_SETTINGS: `${URL_PREFIX}/controls/mac-settings/custom-settings`, CONTROLS_CUSTOM_SETTINGS: `${URL_PREFIX}/controls/mac-settings/custom-settings`,
CONTROLS_MAC_SCRIPTS: `${URL_PREFIX}/controls/mac-scripts`,
DASHBOARD: `${URL_PREFIX}/dashboard`, DASHBOARD: `${URL_PREFIX}/dashboard`,
DASHBOARD_LINUX: `${URL_PREFIX}/dashboard/linux`, DASHBOARD_LINUX: `${URL_PREFIX}/dashboard/linux`,
DASHBOARD_MAC: `${URL_PREFIX}/dashboard/mac`, DASHBOARD_MAC: `${URL_PREFIX}/dashboard/mac`,