mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a75b0de3e |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="14" fill="#111827"/>
|
||||
<path d="M17 20.5L23.2 43.5H28.1L32 29.3L35.9 43.5H40.8L47 20.5H41.7L38.2 35.6L34.3 20.5H29.7L25.8 35.6L22.3 20.5H17Z" fill="#7DD3FC"/>
|
||||
<path d="M45.5 43.5H51V20.5H46.9L41.8 24.1L44.2 28.1L45.5 27.2V43.5Z" fill="#F8FAFC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 386 B |
@@ -18,6 +18,18 @@ export interface IntuneIntegration {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceOneIntegration {
|
||||
api_url: string;
|
||||
token_url: string;
|
||||
client_id: string;
|
||||
client_secret?: string;
|
||||
api_key?: string;
|
||||
last_synced_interval: number;
|
||||
last_synced_at?: string;
|
||||
groups: string[] | Group[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface SentinelOneIntegration {
|
||||
api_token: string;
|
||||
api_url: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useIntegrations } from "../idp-sync/useIntegrations";
|
||||
import { Intune } from "./intune/Intune";
|
||||
import { FleetDM } from "@/modules/integrations/edr/fleetdm/FleetDM";
|
||||
import { Huntress } from "@/modules/integrations/edr/huntress/Huntress";
|
||||
import { WorkspaceOne } from "@/modules/integrations/edr/workspace-one/WorkspaceOne";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
@@ -67,6 +68,7 @@ export default function EDRTab({ account }: Props) {
|
||||
<SentinelOne account={account} />
|
||||
<Huntress account={account} />
|
||||
<FleetDM account={account} />
|
||||
<WorkspaceOne account={account} />
|
||||
</div>
|
||||
</LockedFeatureOverlay>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
HuntressIntegration,
|
||||
IntuneIntegration,
|
||||
SentinelOneIntegration,
|
||||
WorkspaceOneIntegration,
|
||||
} from "@/interfaces/EDR";
|
||||
|
||||
export const useIntegrations = () => {
|
||||
@@ -52,18 +53,28 @@ export const useIntegrations = () => {
|
||||
permission?.edr?.read,
|
||||
);
|
||||
|
||||
const { data: workspaceOne, isLoading: isWorkspaceOneLoading } =
|
||||
useFetchApi<WorkspaceOneIntegration>(
|
||||
"/integrations/edr/workspaceone",
|
||||
false,
|
||||
false,
|
||||
permission?.edr?.read,
|
||||
);
|
||||
|
||||
const isCrowdstrikeEnabled = !!crowdstrike?.enabled;
|
||||
const isIntuneEnabled = !!intune?.enabled;
|
||||
const isSentinelOneEnabled = !!sentinelOne?.enabled;
|
||||
const isHuntressEnabled = !!huntress?.enabled;
|
||||
const isFleetDMEnabled = !!fleetdm?.enabled;
|
||||
const isWorkspaceOneEnabled = !!workspaceOne?.enabled;
|
||||
|
||||
const isAnyIntegrationEnabled =
|
||||
isCrowdstrikeEnabled ||
|
||||
isIntuneEnabled ||
|
||||
isSentinelOneEnabled ||
|
||||
isHuntressEnabled ||
|
||||
isFleetDMEnabled;
|
||||
isFleetDMEnabled ||
|
||||
isWorkspaceOneEnabled;
|
||||
|
||||
const activeIntegrationName = useMemo(() => {
|
||||
if (isCrowdstrikeEnabled) return "CrowdStrike";
|
||||
@@ -71,6 +82,7 @@ export const useIntegrations = () => {
|
||||
if (isSentinelOneEnabled) return "SentinelOne";
|
||||
if (isHuntressEnabled) return "Huntress";
|
||||
if (isFleetDMEnabled) return "FleetDM";
|
||||
if (isWorkspaceOneEnabled) return "Workspace ONE";
|
||||
return "";
|
||||
}, [
|
||||
isCrowdstrikeEnabled,
|
||||
@@ -78,6 +90,7 @@ export const useIntegrations = () => {
|
||||
isSentinelOneEnabled,
|
||||
isHuntressEnabled,
|
||||
isFleetDMEnabled,
|
||||
isWorkspaceOneEnabled,
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -101,6 +114,10 @@ export const useIntegrations = () => {
|
||||
isFleetDMLoading,
|
||||
isFleetDMEnabled,
|
||||
|
||||
workspaceOne,
|
||||
isWorkspaceOneLoading,
|
||||
isWorkspaceOneEnabled,
|
||||
|
||||
isAnyIntegrationEnabled,
|
||||
activeIntegrationName,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import Button from "@components/Button";
|
||||
import { notify } from "@components/Notification";
|
||||
import { SkeletonIntegration } from "@components/skeletons/SkeletonIntegration";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import dayjs from "dayjs";
|
||||
import { isEmpty } from "lodash";
|
||||
import { HistoryIcon, Settings } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import integrationImage from "@/assets/integrations/workspace-one.svg";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
import { WorkspaceOneIntegration } from "@/interfaces/EDR";
|
||||
import { Group } from "@/interfaces/Group";
|
||||
import { IntegrationCard } from "@/modules/integrations/IntegrationCard";
|
||||
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
||||
import WorkspaceOneConfiguration from "@/modules/integrations/edr/workspace-one/WorkspaceOneConfiguration";
|
||||
import WorkspaceOneSetup from "@/modules/integrations/edr/workspace-one/WorkspaceOneSetup";
|
||||
|
||||
type Props = {
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export const WorkspaceOne = ({ account }: Props) => {
|
||||
const { mutate } = useSWRConfig();
|
||||
const [setupModal, setSetupModal] = useState(false);
|
||||
const { permission } = usePermissions();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const {
|
||||
workspaceOne: integration,
|
||||
isAnyIntegrationEnabled,
|
||||
isWorkspaceOneLoading: isLoading,
|
||||
} = useIntegrations();
|
||||
const integrationRequest = useApiCall<WorkspaceOneIntegration>(
|
||||
"/integrations/edr/workspaceone",
|
||||
);
|
||||
|
||||
const [enabled, setEnabled] = useState(!!integration?.enabled);
|
||||
|
||||
useEffect(() => {
|
||||
setEnabled(!!integration?.enabled);
|
||||
}, [integration]);
|
||||
|
||||
const toggleSwitch = async () => {
|
||||
if (!integration?.api_url) return setSetupModal(true);
|
||||
const isCurrentlyEnabled = integration.enabled;
|
||||
|
||||
const choice = isCurrentlyEnabled
|
||||
? await confirm({
|
||||
title: `Disable Workspace ONE?`,
|
||||
description: `Are you sure you want to disable the Workspace ONE integration?`,
|
||||
confirmText: "Disable",
|
||||
cancelText: "Cancel",
|
||||
type: "warning",
|
||||
})
|
||||
: true;
|
||||
if (!choice) return;
|
||||
|
||||
const groups =
|
||||
integration.groups?.map((group) => (group as Group).id) || [];
|
||||
|
||||
notify({
|
||||
title: "Workspace ONE Integration",
|
||||
description: `Workspace ONE was successfully ${
|
||||
isCurrentlyEnabled ? "disabled" : "enabled"
|
||||
}`,
|
||||
promise: integrationRequest
|
||||
.put({
|
||||
...integration,
|
||||
groups,
|
||||
client_secret: undefined,
|
||||
api_key: undefined,
|
||||
enabled: !isCurrentlyEnabled,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/integrations/edr/workspaceone");
|
||||
}),
|
||||
loadingMessage: "Updating integration...",
|
||||
});
|
||||
};
|
||||
|
||||
return isLoading ? (
|
||||
<SkeletonIntegration loadingHeight={196} />
|
||||
) : (
|
||||
<>
|
||||
<IntegrationCard
|
||||
name={"Workspace ONE"}
|
||||
description="Unified endpoint management platform for enforcing access based on Workspace ONE device compliance."
|
||||
url={{
|
||||
title: "omnissa.com",
|
||||
href: "https://www.omnissa.com/products/workspace-one-unified-endpoint-management/",
|
||||
}}
|
||||
image={integrationImage}
|
||||
data={integration?.api_url ? integration : undefined}
|
||||
disabled={
|
||||
enabled
|
||||
? !permission.edr.update
|
||||
: isAnyIntegrationEnabled || !permission.edr.create
|
||||
}
|
||||
switchState={enabled}
|
||||
onEnabledChange={toggleSwitch}
|
||||
onSetup={() => setSetupModal(true)}
|
||||
>
|
||||
{integration && <ConfigurationButton config={integration} />}
|
||||
</IntegrationCard>
|
||||
<WorkspaceOneSetup
|
||||
account={account}
|
||||
open={setupModal}
|
||||
onOpenChange={setSetupModal}
|
||||
onSuccess={() => setEnabled(true)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type ConfigurationProps = {
|
||||
config: WorkspaceOneIntegration;
|
||||
};
|
||||
|
||||
const ConfigurationButton = ({ config }: ConfigurationProps) => {
|
||||
const [configModal, setConfigModal] = useState(false);
|
||||
|
||||
const lastSync = useMemo(() => {
|
||||
if (isEmpty(config?.last_synced_at)) return "Not synchronized";
|
||||
return "Synced " + dayjs().to(config?.last_synced_at);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex gap-2 justify-between"}>
|
||||
<div
|
||||
className={
|
||||
"text-xs flex gap-2 items-center text-nb-gray-300 font-medium"
|
||||
}
|
||||
>
|
||||
<HistoryIcon size={14} />
|
||||
{lastSync}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size={"xs"}
|
||||
className={"items-center"}
|
||||
onClick={() => {
|
||||
setConfigModal(true);
|
||||
}}
|
||||
>
|
||||
<Settings size={14} />
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
<WorkspaceOneConfiguration
|
||||
open={configModal}
|
||||
onOpenChange={setConfigModal}
|
||||
config={config}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,458 @@
|
||||
import Button from "@components/Button";
|
||||
import HelpText from "@components/HelpText";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Label } from "@components/Label";
|
||||
import {
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from "@components/modal/Modal";
|
||||
import { notify } from "@components/Notification";
|
||||
import Paragraph from "@components/Paragraph";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/Tabs";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import { useHasChanges } from "@hooks/useHasChanges";
|
||||
import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import {
|
||||
AlertOctagon,
|
||||
Cog,
|
||||
ExternalLinkIcon,
|
||||
FolderGit2,
|
||||
GlobeIcon,
|
||||
KeyRound,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import integrationImage from "@/assets/integrations/workspace-one.svg";
|
||||
import FullTooltip from "@/components/FullTooltip";
|
||||
import { PeerGroupSelector } from "@/components/PeerGroupSelector";
|
||||
import { useDialog } from "@/contexts/DialogProvider";
|
||||
import { useGroups } from "@/contexts/GroupsProvider";
|
||||
import { WorkspaceOneIntegration } from "@/interfaces/EDR";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
config: WorkspaceOneIntegration;
|
||||
};
|
||||
|
||||
export default function WorkspaceOneConfiguration({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
config,
|
||||
}: Props) {
|
||||
const { isLoading } = useGroups();
|
||||
|
||||
return (
|
||||
!isLoading && (
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
<ConfigurationContent
|
||||
onSuccess={() => {
|
||||
onOpenChange(false);
|
||||
onSuccess && onSuccess();
|
||||
}}
|
||||
config={config}
|
||||
/>
|
||||
</Modal>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
type ModalProps = {
|
||||
onSuccess: () => void;
|
||||
config: WorkspaceOneIntegration;
|
||||
};
|
||||
|
||||
export function ConfigurationContent({
|
||||
onSuccess,
|
||||
config,
|
||||
}: Readonly<ModalProps>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const { confirm } = useDialog();
|
||||
|
||||
const [tab, setTab] = useState<string>("peer-approval");
|
||||
|
||||
const integrationRequest = useApiCall<WorkspaceOneIntegration>(
|
||||
"/integrations/edr/workspaceone",
|
||||
);
|
||||
|
||||
const [apiUrl, setApiUrl] = useState(config.api_url || "");
|
||||
const [tokenUrl, setTokenUrl] = useState(config.token_url || "");
|
||||
const [clientId, setClientId] = useState(config.client_id || "");
|
||||
const secretPlaceholder = "******************************";
|
||||
const [clientSecret, setClientSecret] = useState(secretPlaceholder);
|
||||
const [apiKey, setApiKey] = useState(secretPlaceholder);
|
||||
const [lastSyncedInterval, setLastSyncedInterval] = useState(
|
||||
config.last_synced_interval?.toString() || "24",
|
||||
);
|
||||
|
||||
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
|
||||
initial: config.groups.map((g) => {
|
||||
const isString = typeof g === "string";
|
||||
return isString ? g : g.id;
|
||||
}) as string[],
|
||||
});
|
||||
|
||||
const deleteIntegration = async () => {
|
||||
const choice = await confirm({
|
||||
title: `Delete integration?`,
|
||||
description: "Are you sure you want to delete this integration?",
|
||||
confirmText: "Delete",
|
||||
cancelText: "Cancel",
|
||||
type: "danger",
|
||||
});
|
||||
|
||||
if (!choice) return;
|
||||
|
||||
notify({
|
||||
title: "Workspace ONE Integration",
|
||||
description: `Workspace ONE was successfully deleted`,
|
||||
promise: integrationRequest.del({}).then(() => {
|
||||
mutate("/integrations/edr/workspaceone");
|
||||
onSuccess();
|
||||
}),
|
||||
loadingMessage: "Deleting integration...",
|
||||
});
|
||||
};
|
||||
|
||||
const updateIntegration = async () => {
|
||||
const savedGroups = await saveGroups();
|
||||
|
||||
notify({
|
||||
title: "Workspace ONE Integration",
|
||||
description: `Workspace ONE was successfully updated`,
|
||||
promise: integrationRequest
|
||||
.put({
|
||||
api_url: apiUrl,
|
||||
token_url: tokenUrl,
|
||||
client_id: clientId,
|
||||
client_secret:
|
||||
secretPlaceholder === clientSecret ? undefined : clientSecret,
|
||||
api_key: secretPlaceholder === apiKey ? undefined : apiKey,
|
||||
groups: savedGroups.map((group) => group.id) || [],
|
||||
last_synced_interval: Number(lastSyncedInterval || 24),
|
||||
enabled: config.enabled,
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/integrations/edr/workspaceone");
|
||||
onSuccess();
|
||||
}),
|
||||
loadingMessage: "Updating integration...",
|
||||
});
|
||||
};
|
||||
|
||||
const apiUrlError = useMemo(() => {
|
||||
if (apiUrl === "") return "";
|
||||
if (!validator.isValidUrl(apiUrl)) {
|
||||
return "Please enter a valid url, e.g., https://cn123.awmdm.com";
|
||||
}
|
||||
return "";
|
||||
}, [apiUrl]);
|
||||
|
||||
const tokenUrlError = useMemo(() => {
|
||||
if (tokenUrl === "") return "";
|
||||
if (!validator.isValidUrl(tokenUrl)) {
|
||||
return "Please enter a valid OAuth token URL";
|
||||
}
|
||||
return "";
|
||||
}, [tokenUrl]);
|
||||
|
||||
const { hasChanges } = useHasChanges([
|
||||
apiUrl,
|
||||
tokenUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
apiKey,
|
||||
groups,
|
||||
lastSyncedInterval,
|
||||
]);
|
||||
|
||||
const canSave =
|
||||
hasChanges &&
|
||||
groups.length &&
|
||||
apiUrl.length &&
|
||||
tokenUrl.length &&
|
||||
clientId.length &&
|
||||
apiUrlError === "" &&
|
||||
tokenUrlError === "";
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
maxWidthClass={cn("relative max-w-[650px]")}
|
||||
showClose={true}
|
||||
className={""}
|
||||
autoFocus={false}
|
||||
>
|
||||
<GradientFadedBackground />
|
||||
|
||||
<IntegrationModalHeader
|
||||
image={integrationImage}
|
||||
title={"Workspace ONE Configuration"}
|
||||
description={
|
||||
"Restrict network access to devices based on Workspace ONE UEM compliance."
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
defaultValue={tab}
|
||||
onValueChange={(v) => setTab(v)}
|
||||
className={"mt-6"}
|
||||
>
|
||||
<TabsList justify={"start"} className={"px-8"}>
|
||||
<TabsTrigger value={"peer-approval"}>
|
||||
<FolderGit2
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Peer Approval
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"settings"}>
|
||||
<Cog
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value={"danger"}>
|
||||
<AlertOctagon
|
||||
size={14}
|
||||
className={
|
||||
"text-nb-gray-500 group-data-[state=active]/trigger:text-netbird transition-all"
|
||||
}
|
||||
/>
|
||||
Danger Zone
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value={"peer-approval"} className={"px-8"}>
|
||||
<div>
|
||||
<Label>
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<FolderGit2 size={14} />
|
||||
Groups
|
||||
</div>
|
||||
</Label>
|
||||
<HelpText className={"mt-2"}>
|
||||
Select groups you want to apply the Workspace ONE integration to
|
||||
</HelpText>
|
||||
|
||||
<PeerGroupSelector
|
||||
values={groups}
|
||||
onChange={setGroups}
|
||||
showResourceCounter={false}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"settings"} className={"px-8 text-sm"}>
|
||||
<div className={"mb-3 flex flex-row gap-3 w-full justify-between"}>
|
||||
<div>
|
||||
<Label>
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<RefreshCcw size={14} />
|
||||
Sync Window
|
||||
</div>
|
||||
</Label>
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
content={
|
||||
<div className={"max-w-xs text-xs"}>
|
||||
Example: This property is set to 24 hours. A laptop has not
|
||||
synced with Workspace ONE for 27 hours. Even if the previous
|
||||
state was compliant, it will be blocked from network access.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HelpText className={"max-w-sm mt-1"}>
|
||||
Devices not synced with Workspace ONE in this time won't
|
||||
have network access.
|
||||
<IconInfoCircle
|
||||
size={14}
|
||||
className={"relative inline ml-1 -top-[1px]"}
|
||||
/>
|
||||
</HelpText>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
placeholder={"24"}
|
||||
min={24}
|
||||
max={336}
|
||||
className={"w-full min-w-[130px] ml-auto"}
|
||||
value={lastSyncedInterval}
|
||||
type={"number"}
|
||||
onChange={(e) => setLastSyncedInterval(e.target.value)}
|
||||
customSuffix={"Hours"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={"flex-col gap-3 flex"}>
|
||||
<Input
|
||||
autoCorrect={"off"}
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[165px] flex gap-2 items-center"}>
|
||||
<GlobeIcon size={16} />
|
||||
API URL
|
||||
</div>
|
||||
}
|
||||
placeholder={"https://cn123.awmdm.com"}
|
||||
value={apiUrl}
|
||||
error={apiUrlError}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
autoCorrect={"off"}
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[165px] flex gap-2 items-center"}>
|
||||
<GlobeIcon size={16} />
|
||||
Token URL
|
||||
</div>
|
||||
}
|
||||
placeholder={"https://auth.example.com/connect/token"}
|
||||
value={tokenUrl}
|
||||
error={tokenUrlError}
|
||||
onChange={(e) => setTokenUrl(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
autoCorrect={"off"}
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[165px] flex gap-2 items-center"}>
|
||||
<KeyRound size={16} />
|
||||
Client ID
|
||||
</div>
|
||||
}
|
||||
placeholder={"OAuth client ID"}
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
autoCorrect={"off"}
|
||||
type={"password"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[165px] flex gap-2 items-center"}>
|
||||
<KeyRound size={16} />
|
||||
Client Secret
|
||||
</div>
|
||||
}
|
||||
placeholder={"OAuth client secret"}
|
||||
value={clientSecret}
|
||||
onFocus={() => {
|
||||
if (clientSecret == secretPlaceholder) {
|
||||
setClientSecret("");
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!clientSecret.length) {
|
||||
setClientSecret(secretPlaceholder);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setClientSecret(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
autoCorrect={"off"}
|
||||
type={"password"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[165px] flex gap-2 items-center"}>
|
||||
<KeyRound size={16} />
|
||||
REST API Key
|
||||
</div>
|
||||
}
|
||||
placeholder={"Workspace ONE REST API key"}
|
||||
value={apiKey}
|
||||
onFocus={() => {
|
||||
if (apiKey == secretPlaceholder) {
|
||||
setApiKey("");
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!apiKey.length) {
|
||||
setApiKey(secretPlaceholder);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value={"danger"} className={"px-8"}>
|
||||
<div>
|
||||
<Label>
|
||||
<div className={"flex gap-2 items-center"}>
|
||||
<AlertOctagon size={14} />
|
||||
Delete Integration
|
||||
</div>
|
||||
</Label>
|
||||
<HelpText className={"max-w-lg mt-2"}>
|
||||
Deleting this integration will remove the ability to enforce
|
||||
compliance policies from Workspace ONE. If you delete the
|
||||
integration you will need to reconfigure it again.
|
||||
</HelpText>
|
||||
</div>
|
||||
<Button
|
||||
variant={"danger"}
|
||||
size={"xs"}
|
||||
className={"mt-3"}
|
||||
onClick={deleteIntegration}
|
||||
>
|
||||
Delete Integration
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className={"h-6"}></div>
|
||||
|
||||
<ModalFooter className={"items-center gap-4"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://www.omnissa.com/products/workspace-one-unified-endpoint-management/"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Workspace ONE
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-4"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"} className={"w-full"}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={!canSave}
|
||||
onClick={updateIntegration}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
import Button from "@components/Button";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import InlineLink from "@components/InlineLink";
|
||||
import { Input } from "@components/Input";
|
||||
import { Modal, ModalContent, ModalFooter } from "@components/modal/Modal";
|
||||
import { notify } from "@components/Notification";
|
||||
import Steps from "@components/Steps";
|
||||
import { GradientFadedBackground } from "@components/ui/GradientFadedBackground";
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconInfoCircle,
|
||||
} from "@tabler/icons-react";
|
||||
import { useApiCall } from "@utils/api";
|
||||
import { cn, validator } from "@utils/helpers";
|
||||
import { isEmpty } from "lodash";
|
||||
import {
|
||||
Clock4,
|
||||
ExternalLinkIcon,
|
||||
FolderGit2,
|
||||
GlobeIcon,
|
||||
KeyRound,
|
||||
PlusCircle,
|
||||
RefreshCcw,
|
||||
Repeat,
|
||||
Settings2,
|
||||
Shield,
|
||||
} from "lucide-react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useSWRConfig } from "swr";
|
||||
import integrationImage from "@/assets/integrations/workspace-one.svg";
|
||||
import HelpText from "@/components/HelpText";
|
||||
import { PeerGroupSelector } from "@/components/PeerGroupSelector";
|
||||
import { Account } from "@/interfaces/Account";
|
||||
import { WorkspaceOneIntegration } from "@/interfaces/EDR";
|
||||
import useGroupHelper from "@/modules/groups/useGroupHelper";
|
||||
import { IntegrationModalHeader } from "@/modules/integrations/IntegrationModalHeader";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: () => void;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export default function WorkspaceOneSetup({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSuccess,
|
||||
account,
|
||||
}: Props) {
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange} key={open ? 1 : 0}>
|
||||
<SetupContent
|
||||
account={account}
|
||||
onSuccess={() => {
|
||||
onOpenChange(false);
|
||||
onSuccess && onSuccess();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type ModalProps = {
|
||||
onSuccess: () => void;
|
||||
account: Account;
|
||||
};
|
||||
|
||||
export function SetupContent({ onSuccess, account }: Readonly<ModalProps>) {
|
||||
const { mutate } = useSWRConfig();
|
||||
const integrationRequest = useApiCall<WorkspaceOneIntegration>(
|
||||
"/integrations/edr/workspaceone",
|
||||
);
|
||||
const settingsRequest = useApiCall<Account>("/accounts/" + account.id);
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const maxSteps = 4;
|
||||
|
||||
const [apiUrl, setApiUrl] = useState("");
|
||||
const [tokenUrl, setTokenUrl] = useState("");
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [clientSecret, setClientSecret] = useState("");
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [lastSyncedInterval, setLastSyncedInterval] = useState("24");
|
||||
|
||||
const [groups, setGroups, { save: saveGroups }] = useGroupHelper({
|
||||
initial: [],
|
||||
});
|
||||
|
||||
const apiUrlError = useMemo(() => {
|
||||
if (apiUrl === "") return "";
|
||||
if (!validator.isValidUrl(apiUrl)) {
|
||||
return "Please enter a valid url, e.g., https://cn123.awmdm.com";
|
||||
}
|
||||
return "";
|
||||
}, [apiUrl]);
|
||||
|
||||
const tokenUrlError = useMemo(() => {
|
||||
if (tokenUrl === "") return "";
|
||||
if (!validator.isValidUrl(tokenUrl)) {
|
||||
return "Please enter a valid OAuth token URL";
|
||||
}
|
||||
return "";
|
||||
}, [tokenUrl]);
|
||||
|
||||
const urlsEntered = !isEmpty(apiUrl) && !isEmpty(tokenUrl);
|
||||
const credentialsEntered =
|
||||
!isEmpty(clientId) && !isEmpty(clientSecret) && !isEmpty(apiKey);
|
||||
|
||||
const isDisabled =
|
||||
(step == 1 &&
|
||||
(!urlsEntered || apiUrlError !== "" || tokenUrlError !== "")) ||
|
||||
(step == 2 && !credentialsEntered) ||
|
||||
(step == 3 && groups.length == 0);
|
||||
|
||||
const connect = async () => {
|
||||
await settingsRequest.put({
|
||||
id: account.id,
|
||||
settings: {
|
||||
...account.settings,
|
||||
peer_login_expiration_enabled:
|
||||
account?.settings?.peer_login_expiration_enabled,
|
||||
peer_login_expiration: account?.settings?.peer_login_expiration,
|
||||
extra: {
|
||||
...account.settings?.extra,
|
||||
peer_approval_enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const savedGroups = await saveGroups();
|
||||
|
||||
notify({
|
||||
title: "Workspace ONE Integration",
|
||||
description: `Workspace ONE was successfully connected to NetBird.`,
|
||||
promise: integrationRequest
|
||||
.post({
|
||||
api_url: apiUrl,
|
||||
token_url: tokenUrl,
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
api_key: apiKey,
|
||||
groups: savedGroups.map((group) => group.id) || [],
|
||||
last_synced_interval: Number(lastSyncedInterval || 24),
|
||||
})
|
||||
.then(() => {
|
||||
mutate("/accounts");
|
||||
mutate("/integrations/edr/workspaceone");
|
||||
onSuccess();
|
||||
}),
|
||||
loadingMessage: "Setting up integration...",
|
||||
});
|
||||
};
|
||||
|
||||
const modalWidth = {
|
||||
0: "max-w-lg",
|
||||
1: "max-w-xl",
|
||||
2: "max-w-xl",
|
||||
3: "max-w-xl",
|
||||
4: "max-w-lg",
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
maxWidthClass={cn(
|
||||
"relative",
|
||||
modalWidth[step as keyof typeof modalWidth],
|
||||
)}
|
||||
showClose={true}
|
||||
className={""}
|
||||
onEscapeKeyDown={(e) => step > 0 && e.preventDefault()}
|
||||
onInteractOutside={(e) => step > 0 && e.preventDefault()}
|
||||
onPointerDownOutside={(e) => step > 0 && e.preventDefault()}
|
||||
>
|
||||
<GradientFadedBackground />
|
||||
|
||||
{step > 0 && (
|
||||
<div className={"flex gap-2 w-full items-center justify-center mb-4"}>
|
||||
{Array.from({ length: maxSteps }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"w-8 h-1 rounded-full bg-nb-gray-800",
|
||||
step >= index + 1 && "bg-netbird",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationModalHeader
|
||||
image={integrationImage}
|
||||
title={"Connect NetBird with Workspace ONE"}
|
||||
description={
|
||||
"Restrict network access to devices that are managed and compliant in Workspace ONE UEM."
|
||||
}
|
||||
/>
|
||||
|
||||
{step == 0 && (
|
||||
<div
|
||||
className={
|
||||
"px-8 py-3 flex z-0 flex-col gap-0 text-sm mb-3 text-center justify-center items-center"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"mt-6 text-base font-medium text-nb-gray-100 flex gap-2 items-center justify-center"
|
||||
}
|
||||
>
|
||||
<Shield size={16} />
|
||||
Required Permissions
|
||||
</div>
|
||||
<p className={"mt-2 !text-nb-gray-300 !leading-[1.5]"}>
|
||||
Ensure that you have a Workspace ONE OAuth client and REST API key
|
||||
that can read devices and compliance status.
|
||||
</p>
|
||||
<div
|
||||
className={
|
||||
"flex items-center flex-col gap-0 mt-2 w-full justify-center max-w-lg"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
|
||||
}
|
||||
>
|
||||
<PlusCircle size={14} className={"text-sky-500"} />
|
||||
OAuth Client Credentials
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"py-2 px-6 flex items-center gap-2 rounded-md w-full justify-center bg-nb-gray-930/0 text-nb-gray-200"
|
||||
}
|
||||
>
|
||||
<Settings2 size={14} className={"text-sky-500"} />
|
||||
Device and Compliance Read Access
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == 1 && (
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<GlobeIcon size={18} />
|
||||
Enter your Workspace ONE URLs
|
||||
</p>
|
||||
|
||||
<Steps>
|
||||
<Steps.Step step={1}>
|
||||
<p>Copy the Workspace ONE UEM REST API base URL.</p>
|
||||
</Steps.Step>
|
||||
|
||||
<Steps.Step step={2} line={false}>
|
||||
<p>Copy the OAuth token URL for the same Workspace ONE tenant.</p>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
|
||||
<div className={"mb-4 flex flex-col gap-3"}>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[115px] flex items-center gap-2"}>
|
||||
<GlobeIcon size={14} />
|
||||
API URL
|
||||
</div>
|
||||
}
|
||||
placeholder={"https://cn123.awmdm.com"}
|
||||
value={apiUrl}
|
||||
error={apiUrlError}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[115px] flex items-center gap-2"}>
|
||||
<GlobeIcon size={14} />
|
||||
Token URL
|
||||
</div>
|
||||
}
|
||||
placeholder={"https://auth.example.com/connect/token"}
|
||||
value={tokenUrl}
|
||||
error={tokenUrlError}
|
||||
onChange={(e) => setTokenUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == 2 && (
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 mt-4"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<KeyRound size={20} />
|
||||
Enter Workspace ONE credentials
|
||||
</p>
|
||||
<Steps>
|
||||
<Steps.Step step={1}>
|
||||
<p className={"font-normal"}>
|
||||
Create or select an OAuth client in Workspace ONE with device
|
||||
read access.
|
||||
</p>
|
||||
</Steps.Step>
|
||||
<Steps.Step step={2} line={false}>
|
||||
<p className={"font-normal"}>
|
||||
Paste the client credentials and REST API key below.
|
||||
</p>
|
||||
</Steps.Step>
|
||||
</Steps>
|
||||
|
||||
<div className={"mb-4 flex flex-col gap-3"}>
|
||||
<Input
|
||||
type={"text"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[135px] flex items-center gap-2"}>
|
||||
<KeyRound size={16} className={"text-nb-gray-300"} />
|
||||
Client ID
|
||||
</div>
|
||||
}
|
||||
placeholder={"OAuth client ID"}
|
||||
value={clientId}
|
||||
onChange={(e) => setClientId(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={"password"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[135px] flex items-center gap-2"}>
|
||||
<KeyRound size={16} className={"text-nb-gray-300"} />
|
||||
Client Secret
|
||||
</div>
|
||||
}
|
||||
placeholder={"OAuth client secret"}
|
||||
value={clientSecret}
|
||||
onChange={(e) => setClientSecret(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type={"password"}
|
||||
className={"w-full"}
|
||||
customPrefix={
|
||||
<div className={"min-w-[135px] flex items-center gap-2"}>
|
||||
<KeyRound size={16} className={"text-nb-gray-300"} />
|
||||
REST API Key
|
||||
</div>
|
||||
}
|
||||
placeholder={"Workspace ONE REST API key"}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == 3 && (
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 mt-4 mb-3"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<FolderGit2 size={16} />
|
||||
Peer Approval
|
||||
</p>
|
||||
|
||||
<HelpText className={"max-w-lg mt-2"}>
|
||||
Select groups you want to apply the Workspace ONE integration to
|
||||
</HelpText>
|
||||
|
||||
<PeerGroupSelector
|
||||
values={groups}
|
||||
onChange={setGroups}
|
||||
showResourceCounter={false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step == 4 && (
|
||||
<div className={"px-8 py-3 flex flex-col gap-0 mt-4 mb-3"}>
|
||||
<p className={"font-medium flex gap-3 items-center text-base"}>
|
||||
<RefreshCcw size={16} />
|
||||
Workspace ONE Sync Window
|
||||
</p>
|
||||
<div className={"mt-2 flex flex-row gap-3"}>
|
||||
<FullTooltip
|
||||
interactive={false}
|
||||
content={
|
||||
<div className={"max-w-xs text-xs"}>
|
||||
Example: This property is set to 24 hours. A laptop has not
|
||||
synced with Workspace ONE for 27 hours. Even if the previous
|
||||
state was compliant, it will be blocked from network access.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HelpText className={"max-w-lg"}>
|
||||
Devices not synced with Workspace ONE in this time won't
|
||||
have network access.
|
||||
<IconInfoCircle
|
||||
size={14}
|
||||
className={"relative inline ml-1 -top-[1px]"}
|
||||
/>
|
||||
</HelpText>
|
||||
</FullTooltip>
|
||||
<Input
|
||||
placeholder={"24"}
|
||||
min={24}
|
||||
max={336}
|
||||
className={"w-full min-w-[130px]"}
|
||||
value={lastSyncedInterval}
|
||||
type={"number"}
|
||||
onChange={(e) => setLastSyncedInterval(e.target.value)}
|
||||
customSuffix={"Hours"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ModalFooter className={"items-center gap-4"}>
|
||||
{step > 0 && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className={"w-full"}
|
||||
onClick={() => setStep(step - 1)}
|
||||
>
|
||||
<IconArrowLeft size={16} />
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
{step >= 0 && step < maxSteps && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={isDisabled}
|
||||
onClick={() => setStep(step + 1)}
|
||||
>
|
||||
{step == 0 ? "Get Started" : "Continue"}
|
||||
<IconArrowRight size={16} />
|
||||
</Button>
|
||||
)}
|
||||
{step == maxSteps && (
|
||||
<Button
|
||||
variant={"primary"}
|
||||
className={"w-full"}
|
||||
disabled={isDisabled}
|
||||
onClick={connect}
|
||||
>
|
||||
<Repeat size={16} />
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
{step == 0 && (
|
||||
<div
|
||||
className={
|
||||
"text-center z-0 mt-2.5 text-xs text-nb-gray-300 flex items-center justify-center gap-2 font-normal"
|
||||
}
|
||||
>
|
||||
<Clock4 size={12} />
|
||||
<div>
|
||||
Estimated setup time:
|
||||
<span className={"font-medium"}> 5-10 Minutes</span>
|
||||
</div>
|
||||
<InlineLink
|
||||
href={
|
||||
"https://www.omnissa.com/products/workspace-one-unified-endpoint-management/"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</div>
|
||||
)}
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user