Fleet UI: Make preview end user experience look like end user experience (#37349)

This commit is contained in:
RachelElysia
2025-12-16 16:44:34 -05:00
committed by GitHub
parent 5065843ec9
commit 29067be439
12 changed files with 279 additions and 35 deletions
@@ -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>
</>
@@ -106,6 +106,7 @@ const SoftwareAppStoreAndroid = ({
/>
{showPreviewEndUserExperience && (
<CategoriesEndUserExperienceModal
source="android_apps"
onCancel={onClickPreviewEndUserExperience}
/>
)}
@@ -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}
/>
@@ -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);
@@ -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>
@@ -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;
}
@@ -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>
@@ -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";
+6 -2
View File
@@ -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;
}