Create new ListItem and FileUploader component and use on controls page (#15103)
This adds a new ListItem component and FileUploader component and updates the Custom settings and scripts page to use this new ListItem component. This List component centralises where the markup and styles live. We still need to update the bootstrap list item and eula upload list item but will do that in the future. - [x] Manual QA for all new/changed functionality
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import FileUploader from "./FileUploader";
|
||||
|
||||
const meta: Meta<typeof FileUploader> = {
|
||||
title: "Components/FileUploader",
|
||||
component: FileUploader,
|
||||
args: {
|
||||
graphicName: "file-configuration-profile",
|
||||
message: "The main message",
|
||||
additionalInfo: "The additional message",
|
||||
accept: ".pdf",
|
||||
isLoading: false,
|
||||
onFileUpload: () => {
|
||||
alert("File uploaded!");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FileUploader>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
+20
-2
@@ -8,10 +8,23 @@ import Graphic from "components/Graphic";
|
||||
|
||||
const baseClass = "file-uploader";
|
||||
|
||||
type ISupportedGraphicNames = Extract<
|
||||
GraphicNames,
|
||||
| "file-configuration-profile"
|
||||
| "file-sh"
|
||||
| "file-py"
|
||||
| "file-script"
|
||||
| "file-pdf"
|
||||
| "file-pkg"
|
||||
| "file-p7m"
|
||||
| "file-pem"
|
||||
>;
|
||||
|
||||
interface IFileUploaderProps {
|
||||
graphicName: GraphicNames;
|
||||
graphicName: ISupportedGraphicNames;
|
||||
message: string;
|
||||
additionalInfo?: string;
|
||||
/** Controls the loading spinner on the upload button */
|
||||
isLoading?: boolean;
|
||||
/** A comma seperated string of one or more file types accepted to upload.
|
||||
* This is the same as the html accept attribute.
|
||||
@@ -22,6 +35,9 @@ interface IFileUploaderProps {
|
||||
onFileUpload: (files: FileList | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that encapsulates the UI for uploading a file.
|
||||
*/
|
||||
const FileUploader = ({
|
||||
graphicName,
|
||||
message,
|
||||
@@ -37,7 +53,9 @@ const FileUploader = ({
|
||||
<Card color="gray" className={classes}>
|
||||
<Graphic name={graphicName} />
|
||||
<p className={`${baseClass}__message`}>{message}</p>
|
||||
<p className={`${baseClass}__additional-info`}>{additionalInfo}</p>
|
||||
{additionalInfo && (
|
||||
<p className={`${baseClass}__additional-info`}>{additionalInfo}</p>
|
||||
)}
|
||||
<Button
|
||||
className={`${baseClass}__upload-button`}
|
||||
variant="brand"
|
||||
@@ -0,0 +1,6 @@
|
||||
.graphic {
|
||||
// keeps this element the same size as the svg
|
||||
// aligns properly in text, buttons, dropdowns, summary tile custom component
|
||||
display: inline-flex;
|
||||
align-self: center;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
|
||||
import ListItem from "./ListItem";
|
||||
|
||||
const meta: Meta<typeof ListItem> = {
|
||||
title: "Components/ListItem",
|
||||
component: ListItem,
|
||||
args: {
|
||||
graphic: "file-configuration-profile",
|
||||
title: "List Item Title",
|
||||
details: (
|
||||
<>
|
||||
<span>Details </span>
|
||||
<span> • </span>
|
||||
<span> more details</span>
|
||||
</>
|
||||
),
|
||||
actions: (
|
||||
<>
|
||||
<Button>button 1</Button>
|
||||
<Button>Button 2</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof ListItem>;
|
||||
|
||||
export const Basic: Story = {};
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import classnames from "classnames";
|
||||
|
||||
import Graphic from "components/Graphic";
|
||||
import { GraphicNames } from "components/graphics";
|
||||
|
||||
const baseClass = "list-item";
|
||||
|
||||
type ISupportedGraphicNames = Extract<
|
||||
GraphicNames,
|
||||
| "file-configuration-profile"
|
||||
| "file-sh"
|
||||
| "file-py"
|
||||
| "file-script"
|
||||
| "file-pdf"
|
||||
| "file-pkg"
|
||||
| "file-p7m"
|
||||
| "file-pem"
|
||||
>;
|
||||
|
||||
/**
|
||||
* A generic ListItem component that can be used to display a list of items. It
|
||||
* encapsulates the UI logic and styling for displaying a graphic, title,
|
||||
* details, and actions.
|
||||
*/
|
||||
interface IListItemProps {
|
||||
/** The grahpic you want to display for this list item. */
|
||||
graphic: ISupportedGraphicNames;
|
||||
title: string;
|
||||
details: React.ReactNode;
|
||||
/** A collection of React Nodes that will render as list item actions. Can be
|
||||
* used to render buttons, links, etc.
|
||||
*/
|
||||
actions: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ListItem = ({
|
||||
graphic,
|
||||
title,
|
||||
details,
|
||||
actions,
|
||||
className,
|
||||
}: IListItemProps) => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
return (
|
||||
<div className={classNames}>
|
||||
<div className={`${baseClass}__main-content`}>
|
||||
<Graphic name={graphic} />
|
||||
<div className={`${baseClass}__info`}>
|
||||
<span className={`${baseClass}__title`}>{title}</span>
|
||||
<div className={`${baseClass}__details`}>{details}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__actions`}>{actions}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ListItem;
|
||||
@@ -0,0 +1,31 @@
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&__main-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__details {
|
||||
font-size: $xx-small;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $pad-medium;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./ListItem";
|
||||
@@ -55,6 +55,8 @@ export interface IMdmSummaryResponse {
|
||||
mobile_device_management_solution: IMdmSolution[] | null;
|
||||
}
|
||||
|
||||
type SupportedMdmPlatform = "darwin" | "windows";
|
||||
|
||||
export interface IMdmProfile {
|
||||
profile_id: number;
|
||||
team_id: number;
|
||||
|
||||
+48
-64
@@ -1,27 +1,27 @@
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
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 Spinner from "components/Spinner";
|
||||
import DataError from "components/DataError";
|
||||
|
||||
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";
|
||||
import ProfileUploader from "./components/ProfileUploader";
|
||||
|
||||
const baseClass = "custom-settings";
|
||||
|
||||
interface ICustomSettingsProps {
|
||||
currentTeamId: number;
|
||||
/** handler that fires when a change occures on the section (e.g. disk encryption
|
||||
* enabled, profile uploaded) */
|
||||
onMutation: () => void;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ const CustomSettings = ({
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const [showDeleteProfileModal, setShowDeleteProfileModal] = useState(false);
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const selectedProfile = useRef<IMdmProfile | null>(null);
|
||||
|
||||
@@ -41,48 +40,23 @@ const CustomSettings = ({
|
||||
setShowDeleteProfileModal(true);
|
||||
};
|
||||
|
||||
const { data: profiles, refetch: refetchProfiles } = useQuery<
|
||||
IMdmProfilesResponse,
|
||||
unknown,
|
||||
IMdmProfile[] | null
|
||||
>(["profiles", currentTeamId], () => mdmAPI.getProfiles(currentTeamId), {
|
||||
select: (data) => data.profiles,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const onFileUpload = async (files: FileList | null) => {
|
||||
setShowLoading(true);
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
setShowLoading(false);
|
||||
return;
|
||||
const {
|
||||
data: profiles,
|
||||
isLoading: isLoadingProfiles,
|
||||
isError: isErrorProfiles,
|
||||
refetch: refetchProfiles,
|
||||
} = useQuery<IMdmProfilesResponse, unknown, IMdmProfile[] | null>(
|
||||
["profiles", currentTeamId],
|
||||
() => mdmAPI.getProfiles(currentTeamId),
|
||||
{
|
||||
select: (data) => data.profiles,
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const file = files[0];
|
||||
|
||||
if (
|
||||
// file.type might be empty on some systems as uncommon file extensions
|
||||
// would return an empty string.
|
||||
(file.type !== "" && file.type !== "application/x-apple-aspen-config") ||
|
||||
!file.name.includes(".mobileconfig")
|
||||
) {
|
||||
renderFlash("error", UPLOAD_ERROR_MESSAGES.wrongType.message);
|
||||
setShowLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mdmAPI.uploadProfile(file, currentTeamId);
|
||||
refetchProfiles();
|
||||
onMutation();
|
||||
renderFlash("success", "Successfully uploaded!");
|
||||
} catch (e) {
|
||||
const error = e as AxiosResponse<IApiError>;
|
||||
const errMessage = getErrorMessage(error);
|
||||
renderFlash("error", errMessage);
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
const onUploadProfile = () => {
|
||||
refetchProfiles();
|
||||
onMutation();
|
||||
};
|
||||
|
||||
const onCancelDelete = () => {
|
||||
@@ -104,6 +78,30 @@ const CustomSettings = ({
|
||||
}
|
||||
};
|
||||
|
||||
const renderProfileList = () => {
|
||||
if (isLoadingProfiles) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (isErrorProfiles) {
|
||||
return <DataError />;
|
||||
}
|
||||
|
||||
if (!profiles || profiles.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UploadList
|
||||
listItems={profiles}
|
||||
HeadingComponent={ProfileListHeading}
|
||||
ListItemComponent={({ listItem }) => (
|
||||
<ProfileListItem profile={listItem} onDelete={onClickDelete} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<h2>Custom settings</h2>
|
||||
@@ -115,24 +113,10 @@ const CustomSettings = ({
|
||||
url="https://fleetdm.com/docs/using-fleet/mdm-custom-macos-settings"
|
||||
/>
|
||||
</p>
|
||||
|
||||
{profiles && (
|
||||
<UploadList
|
||||
listItems={profiles}
|
||||
HeadingComponent={ProfileListHeading}
|
||||
ListItemComponent={({ listItem }) => (
|
||||
<ProfileListItem profile={listItem} onDelete={onClickDelete} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<FileUploader
|
||||
graphicName="file-configuration-profile"
|
||||
message="Configuration profile (.mobileconfig)"
|
||||
accept=".mobileconfig,application/x-apple-aspen-config"
|
||||
isLoading={showLoading}
|
||||
onFileUpload={onFileUpload}
|
||||
className={`${baseClass}__file-uploader`}
|
||||
{renderProfileList()}
|
||||
<ProfileUploader
|
||||
currentTeamId={currentTeamId}
|
||||
onUpload={onUploadProfile}
|
||||
/>
|
||||
{showDeleteProfileModal && selectedProfile.current && (
|
||||
<DeleteProfileModal
|
||||
|
||||
+39
-29
@@ -7,9 +7,24 @@ import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Icon from "components/Icon";
|
||||
import ListItem from "components/ListItem";
|
||||
|
||||
const baseClass = "profile-list-item";
|
||||
|
||||
interface IProfileDetailsProps {
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const ProfileDetails = ({ createdAt }: IProfileDetailsProps) => {
|
||||
return (
|
||||
<div className={`${baseClass}__profile-details`}>
|
||||
<span className={`${baseClass}__list-item-uploaded`}>
|
||||
{`Uploaded ${formatDistanceToNow(new Date(createdAt))} ago`}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProfileListItemProps {
|
||||
profile: IMdmProfile;
|
||||
onDelete: (profile: IMdmProfile) => void;
|
||||
@@ -25,35 +40,30 @@ const ProfileListItem = ({ profile, onDelete }: IProfileListItemProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__list-item-data`}>
|
||||
<Icon name="profile" />
|
||||
<div className={`${baseClass}__list-item-info`}>
|
||||
<span className={`${baseClass}__list-item-name`}>{profile.name}</span>
|
||||
<span className={`${baseClass}__list-item-uploaded`}>
|
||||
{`Uploaded ${formatDistanceToNow(
|
||||
new Date(profile.created_at)
|
||||
)} ago`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__list-item-actions`}>
|
||||
<Button
|
||||
className={`${baseClass}__list-item-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__list-item-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(profile)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ListItem
|
||||
className={baseClass}
|
||||
graphic="file-configuration-profile"
|
||||
title={profile.name}
|
||||
details={<ProfileDetails createdAt={profile.created_at} />}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(profile)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+17
-1
@@ -1,3 +1,19 @@
|
||||
.profile-list-item {
|
||||
@include list-item;
|
||||
&__profile-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $pad-xsmall;
|
||||
}
|
||||
|
||||
&__list-item-details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $pad-xsmall;
|
||||
font-size: $xx-small;
|
||||
}
|
||||
|
||||
&__action-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
import { IApiError } from "interfaces/errors";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import FileUploader from "components/FileUploader";
|
||||
|
||||
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
|
||||
|
||||
const baseClass = "profile-uploader";
|
||||
|
||||
interface IProfileUploaderProps {
|
||||
currentTeamId: number;
|
||||
onUpload: () => void;
|
||||
}
|
||||
|
||||
const ProfileUploader = ({
|
||||
currentTeamId,
|
||||
onUpload,
|
||||
}: IProfileUploaderProps) => {
|
||||
const [showLoading, setShowLoading] = useState(false);
|
||||
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
|
||||
const onFileUpload = async (files: FileList | null) => {
|
||||
setShowLoading(true);
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
setShowLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
|
||||
if (
|
||||
// file.type might be empty on some systems as uncommon file extensions
|
||||
// would return an empty string.
|
||||
(file.type !== "" && file.type !== "application/x-apple-aspen-config") ||
|
||||
!file.name.includes(".mobileconfig")
|
||||
) {
|
||||
renderFlash("error", UPLOAD_ERROR_MESSAGES.wrongType.message);
|
||||
setShowLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await mdmAPI.uploadProfile(file, currentTeamId);
|
||||
renderFlash("success", "Successfully uploaded!");
|
||||
onUpload();
|
||||
} catch (e) {
|
||||
const error = e as AxiosResponse<IApiError>;
|
||||
const errMessage = getErrorMessage(error);
|
||||
renderFlash("error", errMessage);
|
||||
} finally {
|
||||
setShowLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FileUploader
|
||||
graphicName="file-configuration-profile"
|
||||
message="Configuration profile (.mobileconfig)"
|
||||
accept=".mobileconfig,application/x-apple-aspen-config"
|
||||
isLoading={showLoading}
|
||||
onFileUpload={onFileUpload}
|
||||
className={`${baseClass}__file-uploader`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProfileUploader;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export { default } from "./ProfileUploader";
|
||||
-36
@@ -1,8 +1,4 @@
|
||||
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";
|
||||
|
||||
@@ -17,38 +13,6 @@ const ScriptListHeading = () => {
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.script-list-heading {
|
||||
font-weight: $bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
|
||||
+29
-28
@@ -7,7 +7,7 @@ import scriptAPI, { IScript } from "services/entities/scripts";
|
||||
|
||||
import Icon from "components/Icon";
|
||||
import Button from "components/buttons/Button";
|
||||
import Graphic from "components/Graphic";
|
||||
import ListItem from "components/ListItem";
|
||||
|
||||
const baseClass = "script-list-item";
|
||||
|
||||
@@ -45,33 +45,34 @@ const ScriptListItem = ({ script, onDelete }: IScriptListItemProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__value-group ${baseClass}__script-data`}>
|
||||
<Graphic 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-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(script)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ListItem
|
||||
className={baseClass}
|
||||
graphic={getFileIconName(script.name)}
|
||||
title={script.name}
|
||||
details={
|
||||
<span>{`Uploaded ${formatDistanceToNow(
|
||||
new Date(script.created_at)
|
||||
)} ago`}</span>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={onClickDownload}
|
||||
>
|
||||
<Icon name="download" />
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__action-button`}
|
||||
variant="text-icon"
|
||||
onClick={() => onDelete(script)}
|
||||
>
|
||||
<Icon name="trash" color="ui-fleet-black-75" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,68 +1,6 @@
|
||||
.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 {
|
||||
&__action-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
&__refresh-button,
|
||||
&__download-button {
|
||||
margin-right: $pad-medium;
|
||||
}
|
||||
|
||||
@media (max-width: $break-md) {
|
||||
&__script-statuses {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__script-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@ import { IApiError } from "interfaces/errors";
|
||||
import { NotificationContext } from "context/notification";
|
||||
import scriptAPI from "services/entities/scripts";
|
||||
|
||||
import FileUploader from "pages/ManageControlsPage/components/FileUploader";
|
||||
import FileUploader from "components/FileUploader";
|
||||
|
||||
import { getErrorMessage } from "./helpers";
|
||||
|
||||
const baseClass = "script-uploader";
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ import { NotificationContext } from "context/notification";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
|
||||
import CustomLink from "components/CustomLink";
|
||||
import FileUploader from "pages/ManageControlsPage/components/FileUploader";
|
||||
import FileUploader from "components/FileUploader";
|
||||
|
||||
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
|
||||
|
||||
const baseClass = "bootstrap-package-uploader";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { IApiError } from "interfaces/errors";
|
||||
import mdmAPI from "services/entities/mdm";
|
||||
import { NotificationContext } from "context/notification";
|
||||
|
||||
import FileUploader from "pages/ManageControlsPage/components/FileUploader/FileUploader";
|
||||
import FileUploader from "components/FileUploader/FileUploader";
|
||||
import CustomLink from "components/CustomLink";
|
||||
|
||||
import { UPLOAD_ERROR_MESSAGES, getErrorMessage } from "./helpers";
|
||||
|
||||
@@ -68,6 +68,7 @@ $max-width: 2560px;
|
||||
|
||||
&__list-item-name {
|
||||
font-size: $x-small;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__list-item-uploaded {
|
||||
|
||||
Reference in New Issue
Block a user