Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions frontend/components/CommandPalette/groups/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ const buildControlsItems = (
"windows csp",
],
},
// Assets are premium-only
...(isPremiumTier
? [
{
id: "controls-assets",
label: "Assets",
path: withTeamId(paths.CONTROLS_ASSETS),
keywords: ["assets", "ddm"],
},
]
: []),
// Certificates and Passwords — Premium-only, and not
// available to technicians.
...(isPremiumTier && !isTechnician
Expand Down
1 change: 1 addition & 0 deletions frontend/components/ListItem/ListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type ISupportedGraphicNames = Extract<
| "file-pkg"
| "file-p7m"
| "file-pem"
| "file-json"
| "file-certificate"
>;

Expand Down
68 changes: 68 additions & 0 deletions frontend/components/graphics/FileJson.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from "react";

import { uniqueId } from "lodash";

const FileJson = () => {
const clipPathId = uniqueId("clip-path-");

return (
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="40" fill="none">
<g clipPath={`url(#${clipPathId})`}>
<path
fill="#fff"
stroke="#192147"
strokeWidth={0.5}
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"
/>
<path
fill="#C5C7D1"
d="M23.5.5h.834l.5 6.5 6.666.5v1h-6a2 2 0 0 1-2-2v-6Z"
/>
<path
stroke="#192147"
strokeWidth={0.5}
d="M24.5.334v5.667c0 .736.597 1.333 1.333 1.333h6"
/>
<path
fill="#C5C7D1"
d="M2.5 20h25a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2h-25V20Z"
/>
<rect
width={27.7}
height={16.35}
x={0.25}
y={18.25}
fill="#515774"
rx={1.75}
/>
<text
x={14.1}
y={26.7}
fill="#fff"
fontFamily="Inter, sans-serif"
fontSize={7.5}
fontWeight={700}
textAnchor="middle"
>
json
</text>
<rect
width={27.7}
height={16.35}
x={0.25}
y={18.25}
stroke="#192147"
strokeWidth={0.5}
rx={1.75}
/>
</g>
<defs>
<clipPath id={clipPathId}>
<path fill="#fff" d="M0 0h34v40H0z" />
</clipPath>
</defs>
</svg>
);
};

export default FileJson;
2 changes: 2 additions & 0 deletions frontend/components/graphics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import FilePkg from "./FilePkg";
import FilePng from "./FilePng";
import FileP7m from "./FileP7m";
import FilePem from "./FilePem";
import FileJson from "./FileJson";
import FileVpp from "./FileVpp";
import FileCertificate from "./FileCertificate";
import AppStore from "./AppStore";
Expand Down Expand Up @@ -55,6 +56,7 @@ export const GRAPHIC_MAP = {
"file-png": FilePng,
"file-p7m": FileP7m,
"file-pem": FilePem,
"file-json": FileJson,
"file-vpp": FileVpp,
"file-certificate": FileCertificate,
"app-store": AppStore, // Used in non-editable file uploader for vpp apps edit modal
Expand Down
16 changes: 16 additions & 0 deletions frontend/interfaces/mdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,20 @@ export interface IMdmProfile {
labels_include_all?: IProfileLabel[];
labels_include_any?: IProfileLabel[];
labels_exclude_any?: IProfileLabel[];
// Apple DDM PayloadScope: "User" for user-scoped declarations, "System"
// otherwise. Note this differs from the host details endpoint, which reports

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inconsistency is slightly weird but it's been there for a year so I dunno about changing it now. Happy to be persuaded otherwise(it'd be backend)

// the derived channel as lowercase "user"/"device" (see ProfileScope).
scope?: PayloadScope | null;
}

/** An Apple DDM asset (com.apple.asset.*) that declarations can reference. */
export interface IMdmAsset {
asset_uuid: string;
name: string;
identifier: string;
created_at: string;
uploaded_at: string | null;
checksum: string;
}

export type MdmProfileStatus = "verified" | "verifying" | "pending" | "failed";
Expand All @@ -192,6 +206,8 @@ export type MdmDDMProfileStatus =

export type ProfileOperationType = "remove" | "install";
export type ProfileScope = "device" | "user";
/** Apple DDM declaration PayloadScope as returned by the profiles list endpoint. */
export type PayloadScope = "System" | "User";

export interface IHostMdmProfile {
profile_uuid: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ const OSSettings = ({

const DEFAULT_SETTINGS_SECTION = filteredNavItems[0];

// The "assets" route renders the Configuration profiles card's Assets
// sub-tab, so it resolves to (and keeps the side nav on) that same section.
const isAssetsSubTab = section === "assets";
const effectiveSection = isAssetsSubTab ? "configuration-profiles" : section;

const currentFormSection =
filteredNavItems.find((item) => item.urlSection === section) ??
filteredNavItems.find((item) => item.urlSection === effectiveSection) ??
DEFAULT_SETTINGS_SECTION;

// Redirect to the default section if the URL section is not in the filtered list
Expand Down Expand Up @@ -103,6 +108,7 @@ const OSSettings = ({
onMutation={refetchAggregateProfileStatus}
router={router}
currentPage={currentPage}
activeTab={isAssetsSubTab ? "assets" : "profiles"}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, { useCallback, useContext, useRef, useState } from "react";

import { useQuery } from "react-query";
import { Tab, TabList, TabPanel, Tabs } from "react-tabs";
import PATHS from "router/paths";
import { getPathWithQueryParams } from "utilities/url";

import { AppContext } from "context/app";
import { notify } from "components/ToastNotification";
Expand All @@ -18,6 +20,8 @@ import Spinner from "components/Spinner";
import DataError from "components/DataError";
import EmptyState from "components/EmptyState";
import Button from "components/buttons/Button";
import TabNav from "components/TabNav";
import TabText from "components/TabText";

import Pagination from "components/Pagination";

Expand All @@ -31,20 +35,28 @@ import ProfileListItem from "./components/ProfileListItem";
import UploadListHeading from "../../../components/UploadListHeading";
import ConfigProfileStatusModal from "./components/ConfigProfileStatusModal";
import ResendConfigProfileModal from "./components/ResendConfigProfileModal";
import AssetsTab from "./components/AssetsTab";
import { IOSSettingsCommonProps } from "../../OSSettingsNavItems";

const PROFILES_PER_PAGE = 10;

const baseClass = "configuration-profiles";

export type ConfigProfilesTab = "profiles" | "assets";

const TABS_BY_INDEX: ConfigProfilesTab[] = ["profiles", "assets"];

export type IConfigurationProfilesProps = IOSSettingsCommonProps & {
currentPage?: number;
/** Which secondary tab is active, derived from the route section. */
activeTab?: ConfigProfilesTab;
};

const ConfigurationProfiles = ({
currentTeamId,
router,
currentPage = 0,
activeTab = "profiles",
onMutation,
}: IConfigurationProfilesProps) => {
const {
Expand Down Expand Up @@ -151,6 +163,18 @@ const ConfigurationProfiles = ({
router.push(path.concat(`${queryString}page=${currentPage + 1}`));
}, [router, path, currentPage, queryString]);

const handleTabChange = (index: number) => {
const tabPath =
TABS_BY_INDEX[index] === "assets"
? PATHS.CONTROLS_ASSETS
: PATHS.CONTROLS_CUSTOM_SETTINGS;
router.push(
getPathWithQueryParams(tabPath, {
fleet_id: isPremiumTier ? currentTeamId : undefined,
})
);
};

const onClickInfo = (profile: IMdmProfile) => {
selectedProfile.current = profile;
setShowConfigProfileStatusModal(true);
Expand Down Expand Up @@ -224,38 +248,62 @@ const ConfigurationProfiles = ({
!!profileLabelsModalData?.labels_include_any?.length ||
!!profileLabelsModalData?.labels_exclude_any?.length;

const pageDescription =
activeTab === "assets" ? (
"Manage assets that provide data or credentials referenced by DDM declarations."
) : (
<>
{isTechnician
? "View configuration profiles."
: "Create and upload configuration profiles to apply custom settings."}{" "}
<CustomLink
newTab
text="Learn more"
url="https://fleetdm.com/guides/custom-os-settings"
/>
</>
);

return (
<div className={baseClass}>
<SectionHeader title="Configuration profiles" alignLeftHeaderVertically />
<PageDescription
variant="right-panel"
content={
<>
{isTechnician
? "View configuration profiles."
: "Create and upload configuration profiles to apply custom settings."}{" "}
<CustomLink
newTab
text="Learn more"
url="https://fleetdm.com/guides/custom-os-settings"
/>
</>
}
/>
{!mdmEnabled ? (
<EmptyState
variant="header-list"
header="Additional configuration required"
info="MDM must be turned on to add configuration profiles."
primaryButton={
<Button onClick={() => router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}>
Turn on
</Button>
}
/>
) : (
renderProfileList()
)}
<PageDescription variant="right-panel" content={pageDescription} />
<TabNav secondary>
<Tabs
selectedIndex={TABS_BY_INDEX.indexOf(activeTab)}
onSelect={handleTabChange}
>
<TabList>
<Tab>
<TabText>Profiles</TabText>
</Tab>
<Tab>
<TabText>Assets</TabText>
</Tab>
</TabList>
<TabPanel>
{!mdmEnabled ? (
<EmptyState
variant="header-list"
header="Additional configuration required"
info="MDM must be turned on to add configuration profiles."
primaryButton={
<Button
onClick={() => router.push(PATHS.ADMIN_INTEGRATIONS_MDM)}
>
Turn on
</Button>
}
/>
) : (
renderProfileList()
)}
</TabPanel>
<TabPanel>
<AssetsTab currentTeamId={currentTeamId} router={router} />
</TabPanel>
</Tabs>
</TabNav>
{showAddProfileModal && (
<AddProfileModal
currentTeamId={currentTeamId}
Expand Down
Loading
Loading