Fleet UI: Make preview end user experience look like end user experience (#37349)
This commit is contained in:
@@ -16,6 +16,7 @@ interface ILinkWithContextProps {
|
||||
names: string[];
|
||||
};
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const LinkWithContext = ({
|
||||
@@ -24,6 +25,7 @@ const LinkWithContext = ({
|
||||
to,
|
||||
withParams,
|
||||
className,
|
||||
disabled = false,
|
||||
}: ILinkWithContextProps): JSX.Element => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
||||
@@ -32,6 +34,11 @@ const LinkWithContext = ({
|
||||
const newParams = pick(currentQueryParams, withParams.names);
|
||||
queryString = buildQueryStringFromParams(newParams);
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
return <span className={classNames}>{children}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames}
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface ISearchFieldProps {
|
||||
clearButton?: boolean;
|
||||
icon?: IconNames;
|
||||
tooltip?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SearchField = ({
|
||||
@@ -27,6 +28,7 @@ const SearchField = ({
|
||||
onClick,
|
||||
icon = "search",
|
||||
tooltip,
|
||||
disabled,
|
||||
}: ISearchFieldProps): JSX.Element => {
|
||||
const [searchQueryInput, setSearchQueryInput] = useState(defaultValue);
|
||||
|
||||
@@ -59,6 +61,7 @@ const SearchField = ({
|
||||
clearButton={clearButton}
|
||||
iconPosition="start"
|
||||
iconSvg={icon}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</TooltipWrapper>
|
||||
</>
|
||||
|
||||
+1
@@ -106,6 +106,7 @@ const SoftwareAppStoreAndroid = ({
|
||||
/>
|
||||
{showPreviewEndUserExperience && (
|
||||
<CategoriesEndUserExperienceModal
|
||||
source="android_apps"
|
||||
onCancel={onClickPreviewEndUserExperience}
|
||||
/>
|
||||
)}
|
||||
|
||||
+10
@@ -55,6 +55,9 @@ interface IEditSoftwareModalProps {
|
||||
router: InjectedRouter;
|
||||
openViewYamlModal: () => void;
|
||||
isIosOrIpadosApp?: boolean;
|
||||
name: string;
|
||||
displayName: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
const EditSoftwareModal = ({
|
||||
@@ -67,6 +70,9 @@ const EditSoftwareModal = ({
|
||||
router,
|
||||
openViewYamlModal,
|
||||
isIosOrIpadosApp = false,
|
||||
name,
|
||||
displayName,
|
||||
source,
|
||||
}: IEditSoftwareModalProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const { config } = useContext(AppContext);
|
||||
@@ -372,6 +378,10 @@ const EditSoftwareModal = ({
|
||||
)}
|
||||
{showPreviewEndUserExperienceModal && (
|
||||
<CategoriesEndUserExperienceModal
|
||||
name={name}
|
||||
displayName={displayName}
|
||||
source={source}
|
||||
iconUrl={softwareInstaller.icon_url || undefined}
|
||||
onCancel={togglePreviewEndUserExperienceModal}
|
||||
isIosOrIpadosApp={isIosOrIpadosApp}
|
||||
/>
|
||||
|
||||
+3
@@ -189,6 +189,9 @@ const SoftwareSummaryCard = ({
|
||||
installerType={installerType}
|
||||
openViewYamlModal={onToggleViewYaml}
|
||||
isIosOrIpadosApp={isIosOrIpadosApp}
|
||||
name={softwareTitle.name}
|
||||
displayName={softwareTitle.display_name || softwareTitle.name}
|
||||
source={softwareTitle.source}
|
||||
/>
|
||||
)}
|
||||
{showEditConfigurationModal && softwareInstaller && teamId && (
|
||||
|
||||
@@ -29,6 +29,7 @@ const SoftwareIcon = ({
|
||||
url,
|
||||
uploadedAt,
|
||||
}: ISoftwareIconProps) => {
|
||||
console.log("name", name, "source", source, "url", url);
|
||||
const classNames = classnames(baseClass, `${baseClass}__${size}`);
|
||||
|
||||
const [imgFailed, setImgFailed] = useState(false);
|
||||
|
||||
+180
-12
@@ -1,35 +1,203 @@
|
||||
import React from "react";
|
||||
/** Mobile preview modal uses a screenshot
|
||||
* Non-mobile now uses HTML/CSS instead for
|
||||
* maintainability as the self-selvice UI changes
|
||||
*
|
||||
* Currently only shown from the edit UI, though wired through the Add UI
|
||||
* Users currently can set categories only when editing a curent installer
|
||||
*/
|
||||
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import { Column } from "react-table";
|
||||
import { noop } from "lodash";
|
||||
import { AppContext } from "context/app";
|
||||
import { IHeaderProps } from "interfaces/datatable_config";
|
||||
|
||||
import TableContainer from "components/TableContainer";
|
||||
import SoftwareNameCell from "components/TableContainer/DataTable/SoftwareNameCell";
|
||||
import Modal from "components/Modal";
|
||||
import Button from "components/buttons/Button";
|
||||
import CategoriesEndUserExperiencePreview from "../../../../../../assets/images/categories-end-user-experience-preview-570x259@2x.png";
|
||||
import Card from "components/Card";
|
||||
import SelfServiceHeader from "pages/hosts/details/cards/Software/SelfService/components/SelfServiceHeader";
|
||||
import SearchField from "components/forms/fields/SearchField";
|
||||
import HeaderCell from "components/TableContainer/DataTable/HeaderCell";
|
||||
import CategoriesMenu from "pages/hosts/details/cards/Software/SelfService/components/CategoriesMenu";
|
||||
import { CATEGORIES_NAV_ITEMS } from "pages/hosts/details/cards/Software/SelfService/helpers";
|
||||
|
||||
import CategoriesEndUserExperiencePreviewMobile from "../../../../../../assets/images/categories-end-user-experience-preview-mobile@2x.png";
|
||||
|
||||
const baseClass = "categories-end-user-experience-preview-modal";
|
||||
|
||||
interface ISoftwareRow {
|
||||
name: React.ReactNode;
|
||||
}
|
||||
|
||||
type ITableHeaderProps = IHeaderProps<ISoftwareRow>;
|
||||
|
||||
const columns: Column<ISoftwareRow>[] = [
|
||||
{
|
||||
Header: (cellProps: ITableHeaderProps) => (
|
||||
<HeaderCell value="Name" isSortedDesc={cellProps.column.isSortedDesc} />
|
||||
),
|
||||
accessor: "name",
|
||||
disableSortBy: true,
|
||||
},
|
||||
];
|
||||
|
||||
const getData = (
|
||||
name: string,
|
||||
displayName: string,
|
||||
iconUrl: string | null,
|
||||
source?: string
|
||||
): ISoftwareRow[] => [
|
||||
{
|
||||
name: (
|
||||
<SoftwareNameCell
|
||||
name={name}
|
||||
display_name={displayName}
|
||||
source={source}
|
||||
iconUrl={iconUrl}
|
||||
pageContext="deviceUser"
|
||||
isSelfService
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<SoftwareNameCell
|
||||
name="1Password"
|
||||
source="apps"
|
||||
pageContext="deviceUser"
|
||||
isSelfService
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<SoftwareNameCell
|
||||
name="Adobe Acrobat Reader"
|
||||
source="apps"
|
||||
pageContext="deviceUser"
|
||||
isSelfService
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<SoftwareNameCell
|
||||
name="Box Drive"
|
||||
source="apps"
|
||||
pageContext="deviceUser"
|
||||
isSelfService
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const EmptyState = () => <div>No software found</div>;
|
||||
|
||||
interface BasicSoftwareTableProps {
|
||||
name: string;
|
||||
displayName: string;
|
||||
source?: string;
|
||||
iconUrl?: string | null;
|
||||
}
|
||||
|
||||
const BasicSoftwareTable = ({
|
||||
name,
|
||||
displayName,
|
||||
source,
|
||||
iconUrl = null,
|
||||
}: BasicSoftwareTableProps) => {
|
||||
return (
|
||||
<TableContainer<ISoftwareRow>
|
||||
columnConfigs={columns}
|
||||
data={getData(name, displayName, iconUrl, source)}
|
||||
isLoading={false}
|
||||
emptyComponent={EmptyState}
|
||||
showMarkAllPages={false}
|
||||
isAllPagesSelected={false}
|
||||
searchable={false}
|
||||
disablePagination
|
||||
disableCount
|
||||
disableTableHeader
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICategoriesEndUserExperienceModal {
|
||||
onCancel: () => void;
|
||||
isIosOrIpadosApp?: boolean;
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
iconUrl?: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
const CategoriesEndUserExperienceModal = ({
|
||||
onCancel,
|
||||
isIosOrIpadosApp = false,
|
||||
name = "Software name",
|
||||
displayName = "Software name",
|
||||
iconUrl,
|
||||
source,
|
||||
}: ICategoriesEndUserExperienceModal): JSX.Element => {
|
||||
const { config } = useContext(AppContext);
|
||||
return (
|
||||
<Modal title="End user experience" onExit={onCancel} className={baseClass}>
|
||||
<>
|
||||
<span>What end users see:</span>
|
||||
<div className={`${baseClass}__preview`}>
|
||||
<img
|
||||
src={
|
||||
isIosOrIpadosApp
|
||||
? CategoriesEndUserExperiencePreviewMobile
|
||||
: CategoriesEndUserExperiencePreview
|
||||
}
|
||||
alt="Categories end user experience preview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isIosOrIpadosApp ? (
|
||||
<div className={`${baseClass}__preview`}>
|
||||
<img
|
||||
src={CategoriesEndUserExperiencePreviewMobile}
|
||||
alt="Categories end user experience preview"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Card
|
||||
borderRadiusSize="medium"
|
||||
color="grey"
|
||||
className={`${baseClass}__preview-card`}
|
||||
paddingSize="xlarge"
|
||||
>
|
||||
<div className={`${baseClass}__disabled-overlay`} />
|
||||
<Card
|
||||
className={`${baseClass}__preview-card__self-service`}
|
||||
borderRadiusSize="xxlarge"
|
||||
>
|
||||
<SelfServiceHeader
|
||||
contactUrl={config?.org_info.contact_url || ""}
|
||||
variant="preview"
|
||||
/>
|
||||
<SearchField
|
||||
placeholder="Search by name"
|
||||
onChange={noop}
|
||||
disabled
|
||||
/>
|
||||
<div className={`${baseClass}__table`}>
|
||||
<CategoriesMenu
|
||||
categories={CATEGORIES_NAV_ITEMS}
|
||||
queryParams={{
|
||||
query: "",
|
||||
order_direction: "asc",
|
||||
order_key: "name",
|
||||
page: 0,
|
||||
per_page: 100,
|
||||
}}
|
||||
readOnly
|
||||
/>
|
||||
<BasicSoftwareTable
|
||||
name={name}
|
||||
displayName={displayName}
|
||||
source={source}
|
||||
iconUrl={iconUrl}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
)}
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel}>Done</Button>
|
||||
</div>
|
||||
|
||||
+36
-1
@@ -14,10 +14,45 @@
|
||||
.modal-cta-wrap {
|
||||
margin-top: 0; // Defer to new gap styling
|
||||
}
|
||||
|
||||
&__preview-card {
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
|
||||
.card-header__header {
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
&__self-service {
|
||||
width: 515px;
|
||||
height: 310px;
|
||||
@include vertical-card-layout;
|
||||
}
|
||||
|
||||
.categories-menu {
|
||||
top: 0;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.data-table__table {
|
||||
width: 325px;
|
||||
}
|
||||
}
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&__disabled-overlay {
|
||||
position: absolute;
|
||||
width: 540px;
|
||||
height: 335px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// Pop up preview immediately.
|
||||
.modal__background:has(.calendar-event-preview-modal) {
|
||||
.modal__background:has(.categories-end-user-experience-preview-modal) {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
+4
@@ -13,12 +13,15 @@ export interface ICategoriesMenu {
|
||||
categories: ICategory[];
|
||||
queryParams: SelfServiceQueryParams;
|
||||
className?: string;
|
||||
/** Used for reusing component on self-service preview */
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
const CategoriesMenu = ({
|
||||
categories,
|
||||
queryParams,
|
||||
className,
|
||||
readOnly = false,
|
||||
}: ICategoriesMenu) => {
|
||||
const wrapperClasses = classNames(baseClass, className);
|
||||
|
||||
@@ -47,6 +50,7 @@ const CategoriesMenu = ({
|
||||
category_id: cat.id !== 0 ? cat.id : undefined,
|
||||
}}
|
||||
to={location.pathname}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<TooltipTruncatedText value={cat.label} tooltipPosition="right" />
|
||||
</LinkWithContext>
|
||||
|
||||
+27
-18
@@ -7,23 +7,32 @@ const SelfServiceHeader = ({
|
||||
variant,
|
||||
}: {
|
||||
contactUrl: string;
|
||||
variant?: "mobile-header";
|
||||
}) => (
|
||||
<CardHeader
|
||||
header="Self-service"
|
||||
subheader={
|
||||
<>
|
||||
Install organization-approved apps provided by your IT department.{" "}
|
||||
{contactUrl && (
|
||||
<span>
|
||||
If you need help,{" "}
|
||||
<CustomLink url={contactUrl} text="reach out to IT" newTab />
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
variant={variant}
|
||||
/>
|
||||
);
|
||||
variant?: "mobile-header" | "preview";
|
||||
}) => {
|
||||
const cardHeaderVariant = variant === "mobile-header" ? variant : undefined;
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
header="Self-service"
|
||||
subheader={
|
||||
<>
|
||||
Install organization-approved apps provided by your IT department.{" "}
|
||||
{contactUrl && (
|
||||
<span>
|
||||
If you need help,{" "}
|
||||
<CustomLink
|
||||
url={contactUrl}
|
||||
text="reach out to IT"
|
||||
newTab
|
||||
disableKeyboardNavigation={variant === "preview"}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
variant={cardHeaderVariant}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelfServiceHeader;
|
||||
|
||||
@@ -16,8 +16,7 @@ import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
// therefore not longer needed anywhere else
|
||||
import { generateTableHeaders } from "pages/labels/components/ManualLabelForm/LabelHostTargetTableConfig";
|
||||
|
||||
// @ts-ignore
|
||||
import validateQuery from "components/forms/validators/validate_query";
|
||||
import { validateQuery } from "components/forms/validators/validate_query";
|
||||
|
||||
import { QueryContext } from "context/query";
|
||||
import { AppContext } from "context/app";
|
||||
|
||||
@@ -433,7 +433,9 @@ $max-width: 2560px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
|
||||
a {
|
||||
a,
|
||||
span.link-with-context // Disabled side nav item
|
||||
{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
@@ -442,8 +444,10 @@ $max-width: 2560px;
|
||||
font-weight: $regular;
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $ui-fleet-black-75-over;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user