fleet/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx
Victor Lyuboslavsky 767c594ad8
Updating UI for Okta config (#35204)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34539

Figma:
https://www.figma.com/design/OgQ8SyLK8Sw5thXtF1eiNP/-31909-Conditional-access-w--Okta

Requires backend PR https://github.com/fleetdm/fleet/pull/35526 to view
Apple profile.

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added Okta as a conditional access provider alongside Microsoft Entra
* Users can now configure both identity providers simultaneously or use
either independently
  * Updated configuration interface with new Okta-specific settings
  * Redesigned UI with separate provider cards for improved clarity

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Jacob Shandling <jacob@shandling.dev>
2025-11-18 19:34:59 -06:00

126 lines
3.6 KiB
TypeScript

import React, { useCallback, useContext, useState } from "react";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { useQuery } from "react-query";
import deepDifference from "utilities/deep_difference";
import { NotificationContext } from "context/notification";
import { AppContext } from "context/app";
import configAPI from "services/entities/config";
import { IConfig } from "interfaces/config";
import Spinner from "components/Spinner";
import SideNav from "../components/SideNav";
import getIntegrationSettingsNavItems from "./IntegrationNavItems";
import { DeepPartial } from "../OrgSettingsPage/cards/constants";
const baseClass = "integrations";
interface IIntegrationSettingsPageProps {
router: InjectedRouter;
params: Params;
}
const IntegrationsPage = ({
router,
params,
}: IIntegrationSettingsPageProps) => {
const { renderFlash } = useContext(NotificationContext);
const { isPremiumTier } = useContext(AppContext);
let { section } = params;
const { subsection } = params;
if (!section && !!subsection) {
section = "sso";
}
const [isUpdatingSettings, setIsUpdatingSettings] = useState(false);
// // // settings that live under the integrations page
const {
data: appConfig,
isLoading: isLoadingAppConfig,
refetch: refetchConfig,
} = useQuery<IConfig, Error, IConfig>(
["config"],
() => configAPI.loadAll(),
{}
);
/** The common submission logic for settings that are rendered on the Integrations page, but use
* the common configAPI.update method, the same one used by cards of the OrgSettingsPage */
const onUpdateSettings = useCallback(
async (formUpdates: DeepPartial<IConfig>) => {
if (!appConfig) {
return false;
}
const diff = deepDifference(formUpdates, appConfig);
// If there's no actual change, don't make the API call to update config.
// Still refetch in case settings were changed inside a card (like end-user auth).
if (Object.keys(diff).length === 0) {
refetchConfig();
return true;
}
setIsUpdatingSettings(true);
// send all formUpdates.agent_options because diff overrides all agent options
diff.agent_options = formUpdates.agent_options;
try {
await configAPI.update(diff);
renderFlash("success", "Successfully updated settings.");
refetchConfig();
return true;
} catch (err: unknown) {
renderFlash("error", "Could not update settings");
return false;
} finally {
setIsUpdatingSettings(false);
}
},
[appConfig, refetchConfig, renderFlash]
);
if (!appConfig) return <></>;
const navItems = getIntegrationSettingsNavItems();
const DEFAULT_SETTINGS_SECTION = navItems[0];
const currentSection =
navItems.find((item) => item.urlSection === section) ??
DEFAULT_SETTINGS_SECTION;
const CurrentCard = currentSection.Card;
return (
<div className={`${baseClass}`}>
<SideNav
className={`${baseClass}__side-nav`}
navItems={navItems}
activeItem={currentSection.urlSection}
CurrentCard={
!isLoadingAppConfig && appConfig ? (
<CurrentCard
router={router}
// below props used only by settings-related cards e.g. SSO
appConfig={appConfig}
handleSubmit={onUpdateSettings}
isPremiumTier={isPremiumTier}
isUpdatingSettings={isUpdatingSettings}
subsection={subsection}
/>
) : (
<Spinner />
)
}
/>
</div>
);
};
export default IntegrationsPage;