mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-20 00:08:02 +01:00
chore: SCIM settings UI (#6800)
https://linear.app/unleash/issue/2-1981/ui-allow-users-to-set-up-scim Adds UI for Provisioning (SCIM) settings. ![image](https://github.com/Unleash/unleash/assets/14320932/e24cf4dd-09d5-459d-bf8a-dd75a966d8eb) ![image](https://github.com/Unleash/unleash/assets/14320932/090a8279-1e98-4d4a-8e21-98cf311f1721) ![image](https://github.com/Unleash/unleash/assets/14320932/aca67619-6748-4848-8f1f-4de1deb90860)
This commit is contained in:
parent
48b8df8f4e
commit
032419aa76
@ -11,11 +11,15 @@ import { ADMIN } from '@server/types/permissions';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import { useState } from 'react';
|
||||
import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import { ScimSettings } from './ScimSettings/ScimSettings';
|
||||
|
||||
export const AuthSettings = () => {
|
||||
const { authenticationType } = useUiConfig().uiConfig;
|
||||
const { uiConfig } = useUiConfig();
|
||||
|
||||
const scimEnabled = useUiFlag('scimApi');
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: 'OpenID Connect',
|
||||
@ -36,6 +40,14 @@ export const AuthSettings = () => {
|
||||
].filter(
|
||||
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
|
||||
);
|
||||
|
||||
if (scimEnabled) {
|
||||
tabs.push({
|
||||
label: 'Provisioning (SCIM)',
|
||||
component: <ScimSettings />,
|
||||
});
|
||||
}
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
return (
|
||||
|
144
frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
Normal file
144
frontend/src/component/admin/auth/ScimSettings/ScimSettings.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import type React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button, FormControlLabel, Grid, Switch } from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
import useToast from 'hooks/useToast';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { useScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
|
||||
import { useScimSettingsApi } from 'hooks/api/actions/useScimSettingsApi/useScimSettingsApi';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { ScimTokenGenerationDialog } from './ScimTokenGenerationDialog';
|
||||
import { ScimTokenDialog } from './ScimTokenDialog';
|
||||
|
||||
export const ScimSettings = () => {
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const { settings, refetch } = useScimSettings();
|
||||
const { saveSettings, generateNewToken, errors, loading } =
|
||||
useScimSettingsApi();
|
||||
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
const [tokenGenerationDialog, setTokenGenerationDialog] = useState(false);
|
||||
const [tokenDialog, setTokenDialog] = useState(false);
|
||||
const [newToken, setNewToken] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setEnabled(settings.enabled ?? false);
|
||||
}, [settings]);
|
||||
|
||||
const onSubmit = async (event: React.SyntheticEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
await saveSettings({ enabled });
|
||||
if (enabled && !settings.hasToken) {
|
||||
const token = await generateNewToken();
|
||||
setNewToken(token);
|
||||
setTokenDialog(true);
|
||||
}
|
||||
|
||||
setToastData({
|
||||
title: 'Settings stored',
|
||||
type: 'success',
|
||||
});
|
||||
refetch();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const onGenerateNewToken = async () => {
|
||||
setTokenGenerationDialog(true);
|
||||
};
|
||||
|
||||
const onGenerateNewTokenConfirm = async () => {
|
||||
setTokenGenerationDialog(false);
|
||||
const token = await generateNewToken();
|
||||
setNewToken(token);
|
||||
setTokenDialog(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container sx={{ mb: 3 }}>
|
||||
<Grid item md={12}>
|
||||
<Alert severity='info'>
|
||||
Please read the{' '}
|
||||
<a
|
||||
href='https://docs.getunleash.io/reference/scim'
|
||||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
to learn how to integrate with specific SCIM clients
|
||||
(Microsoft Entra, Okta, etc). <br />
|
||||
SCIM API URL: <code>{uiConfig.unleashUrl}/scim</code>
|
||||
</Alert>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5} mb={2}>
|
||||
<strong>Enable</strong>
|
||||
<p>Enable SCIM provisioning.</p>
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
onChange={(_, enabled) =>
|
||||
setEnabled(enabled)
|
||||
}
|
||||
value={enabled}
|
||||
name='enabled'
|
||||
checked={enabled}
|
||||
/>
|
||||
}
|
||||
label={enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
<Grid item md={5}>
|
||||
<Button
|
||||
variant='contained'
|
||||
color='primary'
|
||||
type='submit'
|
||||
disabled={loading}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(settings.hasToken)}
|
||||
show={
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='error'
|
||||
disabled={loading}
|
||||
onClick={onGenerateNewToken}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
Generate new token
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
<ScimTokenGenerationDialog
|
||||
open={tokenGenerationDialog}
|
||||
setOpen={setTokenGenerationDialog}
|
||||
onConfirm={onGenerateNewTokenConfirm}
|
||||
/>
|
||||
<ScimTokenDialog
|
||||
open={tokenDialog}
|
||||
setOpen={setTokenDialog}
|
||||
token={newToken}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
import { Alert, styled, Typography } from '@mui/material';
|
||||
import { UserToken } from 'component/admin/apiToken/ConfirmToken/UserToken/UserToken';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IScimTokenDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export const ScimTokenDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
token,
|
||||
}: IScimTokenDialogProps) => (
|
||||
<Dialogue
|
||||
open={open}
|
||||
secondaryButtonText='Close'
|
||||
onClose={(_, muiCloseReason?: string) => {
|
||||
if (!muiCloseReason) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
title='SCIM API token created'
|
||||
>
|
||||
<StyledAlert severity='info'>
|
||||
Make sure to copy your SCIM API token now. You won't be able to see
|
||||
it again!
|
||||
</StyledAlert>
|
||||
<Typography variant='body1'>Your token:</Typography>
|
||||
<UserToken token={token || ''} />
|
||||
</Dialogue>
|
||||
);
|
@ -0,0 +1,37 @@
|
||||
import { Alert, styled } from '@mui/material';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
|
||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
||||
marginBottom: theme.spacing(3),
|
||||
}));
|
||||
|
||||
interface IScimTokenGenerationDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const ScimTokenGenerationDialog = ({
|
||||
open,
|
||||
setOpen,
|
||||
onConfirm,
|
||||
}: IScimTokenGenerationDialogProps) => (
|
||||
<Dialogue
|
||||
open={open}
|
||||
secondaryButtonText='Close'
|
||||
onClose={(_, muiCloseReason?: string) => {
|
||||
if (!muiCloseReason) {
|
||||
setOpen(false);
|
||||
}
|
||||
}}
|
||||
primaryButtonText='Generate new token'
|
||||
onClick={onConfirm}
|
||||
title='Generate new SCIM API token?'
|
||||
>
|
||||
<StyledAlert severity='error'>
|
||||
Generating a new token will <strong>immediately revoke</strong> the
|
||||
current one, which may break any existing provision integrations
|
||||
currently using it.
|
||||
</StyledAlert>
|
||||
</Dialogue>
|
||||
);
|
@ -0,0 +1,48 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
import type { ScimSettings } from 'hooks/api/getters/useScimSettings/useScimSettings';
|
||||
|
||||
const ENDPOINT = 'api/admin/scim-settings';
|
||||
|
||||
export type ScimSettingsPayload = Omit<ScimSettings, 'hasToken'>;
|
||||
|
||||
export const useScimSettingsApi = () => {
|
||||
const { loading, makeRequest, createRequest, errors } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const saveSettings = async (scimSettings: ScimSettingsPayload) => {
|
||||
const requestId = 'saveSettings';
|
||||
const req = createRequest(
|
||||
ENDPOINT,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(scimSettings),
|
||||
},
|
||||
requestId,
|
||||
);
|
||||
|
||||
await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const generateNewToken = async (): Promise<string> => {
|
||||
const requestId = 'generateNewToken';
|
||||
const req = createRequest(
|
||||
`${ENDPOINT}/generate-new-token`,
|
||||
{
|
||||
method: 'POST',
|
||||
},
|
||||
requestId,
|
||||
);
|
||||
|
||||
const response = await makeRequest(req.caller, req.id);
|
||||
const { token } = await response.json();
|
||||
return token;
|
||||
};
|
||||
|
||||
return {
|
||||
saveSettings,
|
||||
generateNewToken,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
@ -0,0 +1,46 @@
|
||||
import { useMemo } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
import useUiConfig from '../useUiConfig/useUiConfig';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
const ENDPOINT = 'api/admin/scim-settings';
|
||||
|
||||
export type ScimSettings = {
|
||||
enabled: boolean;
|
||||
hasToken: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_DATA: ScimSettings = {
|
||||
enabled: false,
|
||||
hasToken: false,
|
||||
};
|
||||
|
||||
export const useScimSettings = () => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
const scimEnabled = useUiFlag('scimApi');
|
||||
|
||||
const { data, error, mutate } = useConditionalSWR<ScimSettings>(
|
||||
isEnterprise() && scimEnabled,
|
||||
DEFAULT_DATA,
|
||||
formatApiPath(ENDPOINT),
|
||||
fetcher,
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
settings: data ?? DEFAULT_DATA,
|
||||
loading: !error && !data,
|
||||
refetch: () => mutate(),
|
||||
error,
|
||||
}),
|
||||
[data, error, mutate],
|
||||
);
|
||||
};
|
||||
|
||||
const fetcher = (path: string) => {
|
||||
return fetch(path)
|
||||
.then(handleErrorResponses('SCIM settings'))
|
||||
.then((res) => res.json());
|
||||
};
|
@ -81,6 +81,7 @@ export type UiFlags = {
|
||||
variantDependencies?: boolean;
|
||||
projectOverviewRefactorFeedback?: boolean;
|
||||
featureLifecycle?: boolean;
|
||||
scimApi?: boolean;
|
||||
};
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
Loading…
Reference in New Issue
Block a user