mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-31 03:11:59 +08:00
feat: SAML settings is updated (#1556)
* chore: getFeatureFlag is implemented * feat: authDomain are added
This commit is contained in:
parent
3bbe2f4f58
commit
9372f763c8
@ -9,5 +9,10 @@
|
||||
"add_another_team_member": "Add another team member",
|
||||
"invite_team_members": "Invite team members",
|
||||
"invite_members": "Invite Members",
|
||||
"pending_invites": "Pending Invites"
|
||||
"pending_invites": "Pending Invites",
|
||||
"authenticated_domains": "Authenticated Domains",
|
||||
"delete_domain_message": "Are you sure you want to delete this domain?",
|
||||
"delete_domain": "Delete Domain",
|
||||
"add_domain": "Add Domains",
|
||||
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
|
||||
}
|
||||
|
@ -9,5 +9,10 @@
|
||||
"add_another_team_member": "Add another team member",
|
||||
"invite_team_members": "Invite team members",
|
||||
"invite_members": "Invite Members",
|
||||
"pending_invites": "Pending Invites"
|
||||
"pending_invites": "Pending Invites",
|
||||
"authenticated_domains": "Authenticated Domains",
|
||||
"delete_domain_message": "Are you sure you want to delete this domain?",
|
||||
"delete_domain": "Delete Domain",
|
||||
"add_domain": "Add Domains",
|
||||
"saml_settings":"Your SAML settings have been saved, please login from incognito window to confirm that it has been set up correctly"
|
||||
}
|
||||
|
24
frontend/src/api/SAML/deleteDomain.ts
Normal file
24
frontend/src/api/SAML/deleteDomain.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/SAML/deleteDomain';
|
||||
|
||||
const deleteDomain = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.delete(`/domains/${props.id}`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default deleteDomain;
|
24
frontend/src/api/SAML/listAllDomain.ts
Normal file
24
frontend/src/api/SAML/listAllDomain.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/SAML/listDomain';
|
||||
|
||||
const listAllDomain = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`orgs/${props.orgId}/domains`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default listAllDomain;
|
24
frontend/src/api/SAML/postDomain.ts
Normal file
24
frontend/src/api/SAML/postDomain.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/SAML/postDomain';
|
||||
|
||||
const postDomain = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.post(`/domains`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default postDomain;
|
24
frontend/src/api/SAML/updateDomain.ts
Normal file
24
frontend/src/api/SAML/updateDomain.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps, Props } from 'types/api/SAML/updateDomain';
|
||||
|
||||
const updateDomain = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.put(`/domains/${props.id}`, props);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default updateDomain;
|
24
frontend/src/api/features/getFeatureFlags.ts
Normal file
24
frontend/src/api/features/getFeatureFlags.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/features/getFeaturesFlags';
|
||||
|
||||
const getFeaturesFlags = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const response = await axios.get(`/featureFlags`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.data.status,
|
||||
payload: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getFeaturesFlags;
|
7
frontend/src/constants/featureKeys.ts
Normal file
7
frontend/src/constants/featureKeys.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// keep this consistent with backend model>features.go
|
||||
export enum FeatureKeys {
|
||||
SSO = 'SSO',
|
||||
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
|
||||
BASIC_PLAN = 'BASIC_PLAN',
|
||||
DISABLE_UPSELL = 'DISABLE_UPSELL',
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
import { notification } from 'antd';
|
||||
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import Header from 'container/Header';
|
||||
@ -15,6 +16,7 @@ import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
@ -27,7 +29,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||
const [
|
||||
getUserVersionResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getFeaturesResponse,
|
||||
] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
queryKey: 'getUserVersion',
|
||||
@ -38,9 +44,17 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryKey: 'getUserLatestVersion',
|
||||
enabled: isLoggedIn,
|
||||
},
|
||||
{
|
||||
queryFn: getFeaturesFlags,
|
||||
queryKey: 'getFeatureFlags',
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (getFeaturesResponse.status === 'idle') {
|
||||
getFeaturesResponse.refetch();
|
||||
}
|
||||
|
||||
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
||||
getUserLatestVersionResponse.refetch();
|
||||
}
|
||||
@ -48,7 +62,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
||||
getUserVersionResponse.refetch();
|
||||
}
|
||||
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
||||
}, [
|
||||
getFeaturesResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getUserVersionResponse,
|
||||
isLoggedIn,
|
||||
]);
|
||||
|
||||
const { children } = props;
|
||||
|
||||
@ -121,6 +140,20 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
getFeaturesResponse.isFetched &&
|
||||
getFeaturesResponse.isSuccess &&
|
||||
getFeaturesResponse.data &&
|
||||
getFeaturesResponse.data.payload
|
||||
) {
|
||||
dispatch({
|
||||
type: UPDATE_FEATURE_FLAGS,
|
||||
payload: {
|
||||
...getFeaturesResponse.data.payload,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
isLoggedIn,
|
||||
@ -135,6 +168,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
getUserLatestVersionResponse.isFetched,
|
||||
getUserVersionResponse.isFetched,
|
||||
getUserLatestVersionResponse.isSuccess,
|
||||
getFeaturesResponse.isFetched,
|
||||
getFeaturesResponse.isSuccess,
|
||||
getFeaturesResponse.data,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
@ -0,0 +1,112 @@
|
||||
/* eslint-disable prefer-regex-literals */
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
import { Button, Form, Input, Modal, notification, Typography } from 'antd';
|
||||
import { useForm } from 'antd/es/form/Form';
|
||||
import createDomainApi from 'api/SAML/postDomain';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { Container } from '../styles';
|
||||
|
||||
function AddDomain({ refetch }: Props): JSX.Element {
|
||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
||||
const [isAddDomains, setIsDomain] = useState(false);
|
||||
const [form] = useForm<FormProps>();
|
||||
const SSOFlag = useFeatureFlag(FeatureKeys.SSO);
|
||||
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const onCreateHandler = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await createDomainApi({
|
||||
name: form.getFieldValue('domain'),
|
||||
orgId: (org || [])[0].id,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: 'Your domain has been added successfully.',
|
||||
duration: 15,
|
||||
});
|
||||
setIsDomain(false);
|
||||
refetch();
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container>
|
||||
<Typography.Title level={3}>
|
||||
{t('authenticated_domains', {
|
||||
ns: 'organizationsettings',
|
||||
})}
|
||||
</Typography.Title>
|
||||
{SSOFlag && (
|
||||
<Button
|
||||
onClick={(): void => setIsDomain(true)}
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
>
|
||||
{t('add_domain', { ns: 'organizationsettings' })}
|
||||
</Button>
|
||||
)}
|
||||
</Container>
|
||||
<Modal
|
||||
centered
|
||||
title="Add Domain"
|
||||
footer={null}
|
||||
visible={isAddDomains}
|
||||
destroyOnClose
|
||||
onCancel={(): void => setIsDomain(false)}
|
||||
>
|
||||
<Form form={form} onFinish={onCreateHandler}>
|
||||
<Form.Item
|
||||
required
|
||||
requiredMark
|
||||
name={['domain']}
|
||||
rules={[
|
||||
{
|
||||
message: 'Please enter a valid domain',
|
||||
required: true,
|
||||
pattern: new RegExp(
|
||||
'^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$',
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="signoz.io" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Add Domain
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface FormProps {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export default AddDomain;
|
@ -0,0 +1,39 @@
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
import { IconContainer, TitleContainer } from './styles';
|
||||
|
||||
function Row({
|
||||
onClickHandler,
|
||||
Icon,
|
||||
buttonText,
|
||||
subTitle,
|
||||
title,
|
||||
isDisabled,
|
||||
}: RowProps): JSX.Element {
|
||||
return (
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||
<IconContainer>{Icon}</IconContainer>
|
||||
|
||||
<TitleContainer>
|
||||
<Typography>{title}</Typography>
|
||||
<Typography.Text italic>{subTitle}</Typography.Text>
|
||||
</TitleContainer>
|
||||
|
||||
<Button disabled={isDisabled} onClick={onClickHandler} type="primary">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
export interface RowProps {
|
||||
onClickHandler: VoidFunction;
|
||||
Icon: React.ReactNode;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
buttonText: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
|
||||
export default Row;
|
@ -0,0 +1,11 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div`
|
||||
min-width: 70px;
|
||||
`;
|
@ -0,0 +1,71 @@
|
||||
import { GoogleSquareFilled, KeyOutlined } from '@ant-design/icons';
|
||||
import { Space, Typography } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import Row, { RowProps } from './Row';
|
||||
import { RowContainer } from './styles';
|
||||
|
||||
function Create({
|
||||
setIsSettingsOpen,
|
||||
setIsEditModalOpen,
|
||||
}: CreateProps): JSX.Element {
|
||||
const onConfigureClickHandler = useCallback(() => {
|
||||
console.log('Configure Clicked');
|
||||
}, []);
|
||||
|
||||
const onEditSAMLHandler = useCallback(() => {
|
||||
setIsSettingsOpen(false);
|
||||
setIsEditModalOpen(true);
|
||||
}, [setIsSettingsOpen, setIsEditModalOpen]);
|
||||
|
||||
const data: RowProps[] = [
|
||||
{
|
||||
buttonText: 'Configure',
|
||||
Icon: <GoogleSquareFilled style={{ fontSize: '37px' }} />,
|
||||
title: 'Google Apps Authentication',
|
||||
subTitle: 'Let members sign-in with a Google account',
|
||||
onClickHandler: onConfigureClickHandler,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
buttonText: 'Edit SAML',
|
||||
Icon: <KeyOutlined style={{ fontSize: '37px' }} />,
|
||||
onClickHandler: onEditSAMLHandler,
|
||||
subTitle: 'Azure, Active Directory, Okta or your custom SAML 2.0 solution',
|
||||
title: 'SAML Authentication',
|
||||
isDisabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text italic>
|
||||
SigNoz supports the following single sign-on services (SSO). Get started
|
||||
with setting your project’s SSO below
|
||||
</Typography.Text>
|
||||
|
||||
<RowContainer>
|
||||
<Space direction="vertical">
|
||||
{data.map((rowData) => (
|
||||
<Row
|
||||
Icon={rowData.Icon}
|
||||
buttonText={rowData.buttonText}
|
||||
onClickHandler={rowData.onClickHandler}
|
||||
subTitle={rowData.subTitle}
|
||||
title={rowData.title}
|
||||
key={rowData.title}
|
||||
isDisabled={rowData.isDisabled}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</RowContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface CreateProps {
|
||||
setIsSettingsOpen: (value: boolean) => void;
|
||||
setIsEditModalOpen: (value: boolean) => void;
|
||||
}
|
||||
|
||||
export default Create;
|
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const RowContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
`;
|
@ -0,0 +1,131 @@
|
||||
import { InfoCircleFilled } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
Input,
|
||||
notification,
|
||||
Space,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { useForm } from 'antd/lib/form/Form';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SAMLDomain } from 'types/api/SAML/listDomain';
|
||||
|
||||
function EditSaml({
|
||||
certificate,
|
||||
entityId,
|
||||
url,
|
||||
onRecordUpdateHandler,
|
||||
record,
|
||||
setEditModalOpen,
|
||||
}: EditFormProps): JSX.Element {
|
||||
const [form] = useForm<EditFormProps>();
|
||||
|
||||
const { t } = useTranslation(['common']);
|
||||
|
||||
const onFinishHandler = useCallback(() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(async (values) => {
|
||||
await onRecordUpdateHandler({
|
||||
...record,
|
||||
ssoEnabled: true,
|
||||
samlConfig: {
|
||||
...record.samlConfig,
|
||||
samlCert: values.certificate,
|
||||
samlEntity: values.entityId,
|
||||
samlIdp: values.url,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error({
|
||||
message: t('something_went_wrong', { ns: 'common' }),
|
||||
});
|
||||
});
|
||||
}, [form, onRecordUpdateHandler, record, t]);
|
||||
|
||||
const onResetHandler = useCallback(() => {
|
||||
form.resetFields();
|
||||
setEditModalOpen(false);
|
||||
}, [setEditModalOpen, form]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
name="basic"
|
||||
initialValues={{ certificate, entityId, url }}
|
||||
onFinishFailed={(error): void => {
|
||||
error.errorFields.forEach(({ errors }) => {
|
||||
notification.error({
|
||||
message:
|
||||
errors[0].toString() || t('something_went_wrong', { ns: 'common' }),
|
||||
});
|
||||
});
|
||||
form.resetFields();
|
||||
}}
|
||||
layout="vertical"
|
||||
onFinish={onFinishHandler}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
>
|
||||
<Form.Item
|
||||
label="SAML ACS URL"
|
||||
name="url"
|
||||
rules={[{ required: true, message: 'Please input your ACS URL!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="SAML Entity ID"
|
||||
name="entityId"
|
||||
rules={[{ required: true, message: 'Please input your Entity Id!' }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
rules={[{ required: true, message: 'Please input your Certificate!' }]}
|
||||
label="SAML X.509 Certificate"
|
||||
name="certificate"
|
||||
>
|
||||
<Input.TextArea rows={4} />
|
||||
</Form.Item>
|
||||
|
||||
<Card style={{ marginBottom: '1rem' }}>
|
||||
<Space>
|
||||
<InfoCircleFilled />
|
||||
<Typography>
|
||||
SAML won’t be enabled unless you enter all the attributes above
|
||||
</Typography>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Space
|
||||
style={{ width: '100%', justifyContent: 'flex-end' }}
|
||||
align="end"
|
||||
direction="horizontal"
|
||||
>
|
||||
<Button htmlType="button" onClick={onResetHandler}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save Settings
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditFormProps {
|
||||
url: string;
|
||||
entityId: string;
|
||||
certificate: string;
|
||||
onRecordUpdateHandler: (record: SAMLDomain) => Promise<boolean>;
|
||||
record: SAMLDomain;
|
||||
setEditModalOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export default EditSaml;
|
@ -0,0 +1,49 @@
|
||||
import { Switch } from 'antd';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { SAMLDomain } from 'types/api/SAML/listDomain';
|
||||
|
||||
import { getIsValidCertificate } from '../utils';
|
||||
|
||||
function SwitchComponent({
|
||||
isDefaultChecked,
|
||||
onRecordUpdateHandler,
|
||||
record,
|
||||
}: SwitchComponentProps): JSX.Element {
|
||||
const [isChecked, setIsChecked] = useState<boolean>(isDefaultChecked);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const onChangeHandler = async (checked: boolean): Promise<void> => {
|
||||
setIsLoading(true);
|
||||
const response = await onRecordUpdateHandler({
|
||||
...record,
|
||||
ssoEnabled: checked,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
setIsChecked(checked);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const isInValidCertificate = useMemo(
|
||||
() => !getIsValidCertificate(record?.samlConfig),
|
||||
[record],
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
loading={isLoading}
|
||||
disabled={isInValidCertificate}
|
||||
checked={isChecked}
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface SwitchComponentProps {
|
||||
isDefaultChecked: boolean;
|
||||
onRecordUpdateHandler: (record: SAMLDomain) => Promise<boolean>;
|
||||
record: SAMLDomain;
|
||||
}
|
||||
|
||||
export default SwitchComponent;
|
@ -0,0 +1,296 @@
|
||||
import { LockTwoTone } from '@ant-design/icons';
|
||||
import { Button, Modal, notification, Space, Table, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import deleteDomain from 'api/SAML/deleteDomain';
|
||||
import listAllDomain from 'api/SAML/listAllDomain';
|
||||
import updateDomain from 'api/SAML/updateDomain';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SAMLDomain } from 'types/api/SAML/listDomain';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import AddDomain from './AddDomain';
|
||||
import Create from './Create';
|
||||
import EditSaml from './Edit';
|
||||
import SwitchComponent from './Switch';
|
||||
import { getIsValidCertificate } from './utils';
|
||||
|
||||
function AuthDomains(): JSX.Element {
|
||||
const { t } = useTranslation(['common', 'organizationsettings']);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState<boolean>(false);
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const [currentDomain, setCurrentDomain] = useState<SAMLDomain>();
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
|
||||
const SSOFlag = useFeatureFlag(FeatureKeys.SSO);
|
||||
|
||||
const { data, isLoading, refetch } = useQuery(['saml'], {
|
||||
queryFn: () =>
|
||||
listAllDomain({
|
||||
orgId: (org || [])[0].id,
|
||||
}),
|
||||
enabled: org !== null,
|
||||
});
|
||||
|
||||
const onCloseHandler = useCallback(
|
||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => (): void => {
|
||||
func(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onRecordUpdateHandler = useCallback(
|
||||
async (record: SAMLDomain): Promise<boolean> => {
|
||||
try {
|
||||
const response = await updateDomain(record);
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: t('saml_settings', {
|
||||
ns: 'organizationsettings',
|
||||
}),
|
||||
});
|
||||
refetch();
|
||||
onCloseHandler(setIsEditModalOpen)();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
notification.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t('something_went_wrong', {
|
||||
ns: 'common',
|
||||
}),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[refetch, t, onCloseHandler],
|
||||
);
|
||||
|
||||
const onOpenHandler = useCallback(
|
||||
(func: React.Dispatch<React.SetStateAction<boolean>>) => (): void => {
|
||||
func(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onEditHandler = useCallback(
|
||||
(record: SAMLDomain) => (): void => {
|
||||
setIsEditModalOpen(true);
|
||||
setCurrentDomain(record);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onDeleteHandler = useCallback(
|
||||
(record: SAMLDomain) => (): void => {
|
||||
Modal.confirm({
|
||||
centered: true,
|
||||
title: t('delete_domain', {
|
||||
ns: 'organizationsettings',
|
||||
}),
|
||||
content: t('delete_domain_message', {
|
||||
ns: 'organizationsettings',
|
||||
}),
|
||||
onOk: async () => {
|
||||
const response = await deleteDomain({
|
||||
...record,
|
||||
});
|
||||
|
||||
if (response.statusCode === 200) {
|
||||
notification.success({
|
||||
message: t('common:success'),
|
||||
});
|
||||
refetch();
|
||||
} else {
|
||||
notification.error({
|
||||
message: t('common:something_went_wrong'),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
[refetch, t],
|
||||
);
|
||||
|
||||
const onClickLicenseHandler = useCallback(() => {
|
||||
window.open('http://signoz.io/pricing');
|
||||
}, []);
|
||||
|
||||
const columns: ColumnsType<SAMLDomain> = [
|
||||
{
|
||||
title: 'Domain',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Enforce SSO',
|
||||
dataIndex: 'ssoEnabled',
|
||||
key: 'ssoEnabled',
|
||||
render: (value: boolean, record: SAMLDomain): JSX.Element => {
|
||||
if (!SSOFlag) {
|
||||
return (
|
||||
<Button
|
||||
onClick={onClickLicenseHandler}
|
||||
type="link"
|
||||
icon={<LockTwoTone />}
|
||||
>
|
||||
Upgrade to Configure SSO
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SwitchComponent
|
||||
onRecordUpdateHandler={onRecordUpdateHandler}
|
||||
isDefaultChecked={value}
|
||||
record={record}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
render: (_, record: SAMLDomain): JSX.Element => {
|
||||
if (!SSOFlag) {
|
||||
return (
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
setCurrentDomain(record);
|
||||
onOpenHandler(setIsSettingsOpen)();
|
||||
}}
|
||||
type="link"
|
||||
icon={<LockTwoTone />}
|
||||
>
|
||||
Upgrade to Configure SSO
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const isValidCertificate = getIsValidCertificate(record.samlConfig);
|
||||
|
||||
if (!isValidCertificate) {
|
||||
return <Typography>Configure SSO </Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button type="link" onClick={onEditHandler(record)}>
|
||||
Edit SSO
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
render: (_, record): JSX.Element => {
|
||||
return (
|
||||
<Button
|
||||
disabled={!SSOFlag}
|
||||
onClick={onDeleteHandler(record)}
|
||||
danger
|
||||
type="link"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isLoading && data?.payload?.length === 0) {
|
||||
return (
|
||||
<Space direction="vertical" size="middle">
|
||||
<AddDomain refetch={refetch} />
|
||||
|
||||
<Modal
|
||||
centered
|
||||
title="Configure Authentication Method"
|
||||
onCancel={onCloseHandler(setIsSettingsOpen)}
|
||||
destroyOnClose
|
||||
visible={isSettingsOpen}
|
||||
footer={null}
|
||||
>
|
||||
<Create
|
||||
setIsEditModalOpen={setIsEditModalOpen}
|
||||
setIsSettingsOpen={setIsSettingsOpen}
|
||||
/>
|
||||
</Modal>
|
||||
<Table
|
||||
rowKey={(record: SAMLDomain): string => record.name + v4()}
|
||||
dataSource={[]}
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
centered
|
||||
title="Configure Authentication Method"
|
||||
onCancel={onCloseHandler(setIsSettingsOpen)}
|
||||
destroyOnClose
|
||||
visible={isSettingsOpen}
|
||||
footer={null}
|
||||
>
|
||||
<Create
|
||||
setIsSettingsOpen={setIsSettingsOpen}
|
||||
setIsEditModalOpen={setIsEditModalOpen}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
visible={isEditModalOpen}
|
||||
centered
|
||||
title="Configure SAML"
|
||||
onCancel={onCloseHandler(setIsEditModalOpen)}
|
||||
destroyOnClose
|
||||
style={{ minWidth: '600px' }}
|
||||
footer={null}
|
||||
>
|
||||
<EditSaml
|
||||
certificate={currentDomain?.samlConfig?.samlCert || ''}
|
||||
entityId={currentDomain?.samlConfig?.samlEntity || ''}
|
||||
url={currentDomain?.samlConfig?.samlIdp || ''}
|
||||
onRecordUpdateHandler={onRecordUpdateHandler}
|
||||
record={currentDomain as SAMLDomain}
|
||||
setEditModalOpen={setIsEditModalOpen}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Space direction="vertical" size="middle">
|
||||
<AddDomain refetch={refetch} />
|
||||
|
||||
<Table
|
||||
dataSource={data?.payload || []}
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
rowKey={(record: SAMLDomain): string => record.name + v4()}
|
||||
/>
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthDomains;
|
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
@ -0,0 +1,60 @@
|
||||
import { SAMLDomain } from 'types/api/SAML/listDomain';
|
||||
|
||||
import { getIsValidCertificate } from './utils';
|
||||
|
||||
const inValidCase: SAMLDomain['samlConfig'][] = [
|
||||
{
|
||||
samlCert: '',
|
||||
samlEntity: '',
|
||||
samlIdp: '',
|
||||
},
|
||||
{
|
||||
samlCert: '',
|
||||
samlEntity: '',
|
||||
samlIdp: 'asd',
|
||||
},
|
||||
{
|
||||
samlCert: 'sample certificate',
|
||||
samlEntity: '',
|
||||
samlIdp: '',
|
||||
},
|
||||
{
|
||||
samlCert: 'sample cert',
|
||||
samlEntity: 'sample entity',
|
||||
samlIdp: '',
|
||||
},
|
||||
];
|
||||
|
||||
const validCase: SAMLDomain['samlConfig'][] = [
|
||||
{
|
||||
samlCert: 'sample cert',
|
||||
samlEntity: 'sample entity',
|
||||
samlIdp: 'sample idp',
|
||||
},
|
||||
];
|
||||
|
||||
describe('Utils', () => {
|
||||
inValidCase.forEach((config) => {
|
||||
it('should return invalid saml config', () => {
|
||||
expect(
|
||||
getIsValidCertificate({
|
||||
samlCert: config.samlCert,
|
||||
samlEntity: config.samlEntity,
|
||||
samlIdp: config.samlIdp,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
validCase.forEach((config) => {
|
||||
it('should return invalid saml config', () => {
|
||||
expect(
|
||||
getIsValidCertificate({
|
||||
samlCert: config.samlCert,
|
||||
samlEntity: config.samlEntity,
|
||||
samlIdp: config.samlIdp,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,6 @@
|
||||
export const getIsValidCertificate = (
|
||||
config: Record<string, string>,
|
||||
): boolean =>
|
||||
config?.samlCert.length !== 0 &&
|
||||
config?.samlEntity.length !== 0 &&
|
||||
config?.samlIdp.length !== 0;
|
@ -1,9 +1,12 @@
|
||||
import { Divider, Space } from 'antd';
|
||||
import { FeatureKeys } from 'constants/featureKeys';
|
||||
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import AuthDomains from './AuthDomains';
|
||||
import DisplayName from './DisplayName';
|
||||
import Members from './Members';
|
||||
import PendingInvitesContainer from './PendingInvitesContainer';
|
||||
@ -11,6 +14,9 @@ import PendingInvitesContainer from './PendingInvitesContainer';
|
||||
function OrganizationSettings(): JSX.Element {
|
||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const sso = useFeatureFlag(FeatureKeys.SSO);
|
||||
const noUpsell = useFeatureFlag(FeatureKeys.DISABLE_UPSELL);
|
||||
|
||||
if (!org) {
|
||||
return <div />;
|
||||
}
|
||||
@ -31,6 +37,8 @@ function OrganizationSettings(): JSX.Element {
|
||||
<PendingInvitesContainer />
|
||||
<Divider />
|
||||
<Members />
|
||||
<Divider />
|
||||
{(!noUpsell || (noUpsell && sso)) && <AuthDomains />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { StyledDiv } from 'components/Styled';
|
||||
import { ITraceMetaData } from 'container/GantChart';
|
||||
import { IIntervalUnit, INTERVAL_UNITS } from 'container/TraceDetail/utils';
|
||||
import useThemeMode from 'hooks/useThemeMode';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
import { styles, Svg, TimelineInterval } from './styles';
|
||||
@ -20,10 +20,6 @@ function Timeline({
|
||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||
const { isDarkMode } = useThemeMode();
|
||||
|
||||
const asd = useRef('');
|
||||
|
||||
asd.current = '1';
|
||||
|
||||
const [intervals, setIntervals] = useState<Interval[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
14
frontend/src/hooks/useFeatureFlag.ts
Normal file
14
frontend/src/hooks/useFeatureFlag.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import _get from 'lodash-es/get';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
const useFeatureFlag = (flagKey: string): boolean => {
|
||||
const { featureFlags } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
return _get(featureFlags, flagKey, false);
|
||||
};
|
||||
|
||||
export default useFeatureFlag;
|
@ -10,6 +10,7 @@ import {
|
||||
SWITCH_DARK_MODE,
|
||||
UPDATE_CURRENT_ERROR,
|
||||
UPDATE_CURRENT_VERSION,
|
||||
UPDATE_FEATURE_FLAGS,
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
UPDATE_ORG,
|
||||
@ -54,6 +55,7 @@ const InitialValue: InitialValueTypes = {
|
||||
isUserFetchingError: false,
|
||||
org: null,
|
||||
role: null,
|
||||
featureFlags: null,
|
||||
};
|
||||
|
||||
const appReducer = (
|
||||
@ -194,6 +196,13 @@ const appReducer = (
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_FEATURE_FLAGS: {
|
||||
return {
|
||||
...state,
|
||||
featureFlags: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_ORG: {
|
||||
return {
|
||||
...state,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import {
|
||||
Organization,
|
||||
PayloadProps as OrgPayload,
|
||||
@ -21,6 +22,7 @@ export const UPDATE_USER_ORG_ROLE = 'UPDATE_USER_ORG_ROLE';
|
||||
export const UPDATE_USER = 'UPDATE_USER';
|
||||
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
||||
export const UPDATE_ORG = 'UPDATE_ORG';
|
||||
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
||||
|
||||
export interface SwitchDarkMode {
|
||||
type: typeof SWITCH_DARK_MODE;
|
||||
@ -110,6 +112,11 @@ export interface UpdateOrg {
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateFeatureFlags {
|
||||
type: typeof UPDATE_FEATURE_FLAGS;
|
||||
payload: FeatureFlagPayload;
|
||||
}
|
||||
|
||||
export type AppAction =
|
||||
| SwitchDarkMode
|
||||
| LoggedInUser
|
||||
@ -122,4 +129,5 @@ export type AppAction =
|
||||
| UpdateUserOrgRole
|
||||
| UpdateUser
|
||||
| UpdateOrgName
|
||||
| UpdateOrg;
|
||||
| UpdateOrg
|
||||
| UpdateFeatureFlags;
|
||||
|
5
frontend/src/types/api/SAML/deleteDomain.ts
Normal file
5
frontend/src/types/api/SAML/deleteDomain.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SAMLDomain } from './listDomain';
|
||||
|
||||
export type Props = SAMLDomain;
|
||||
|
||||
export type PayloadProps = SAMLDomain;
|
20
frontend/src/types/api/SAML/listDomain.ts
Normal file
20
frontend/src/types/api/SAML/listDomain.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Organization } from '../user/getOrganization';
|
||||
|
||||
export interface SAMLDomain {
|
||||
id: string;
|
||||
name: string;
|
||||
orgId: Organization['id'];
|
||||
ssoEnabled: boolean;
|
||||
ssoType: 'SAML';
|
||||
samlConfig: {
|
||||
samlEntity: string;
|
||||
samlIdp: string;
|
||||
samlCert: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
orgId: Organization['id'];
|
||||
}
|
||||
|
||||
export type PayloadProps = SAMLDomain[];
|
8
frontend/src/types/api/SAML/postDomain.ts
Normal file
8
frontend/src/types/api/SAML/postDomain.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SAMLDomain } from './listDomain';
|
||||
|
||||
export type Props = {
|
||||
name: string;
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export type PayloadProps = SAMLDomain;
|
5
frontend/src/types/api/SAML/updateDomain.ts
Normal file
5
frontend/src/types/api/SAML/updateDomain.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SAMLDomain } from './listDomain';
|
||||
|
||||
export type Props = SAMLDomain;
|
||||
|
||||
export type PayloadProps = SAMLDomain;
|
3
frontend/src/types/api/features/getFeaturesFlags.ts
Normal file
3
frontend/src/types/api/features/getFeaturesFlags.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export interface PayloadProps {
|
||||
[key: string]: boolean;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||
import { PayloadProps as OrgPayload } from 'types/api/user/getOrganization';
|
||||
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
||||
import { ROLES } from 'types/roles';
|
||||
@ -24,4 +25,5 @@ export default interface AppReducer {
|
||||
isUserFetchingError: boolean;
|
||||
role: ROLES | null;
|
||||
org: OrgPayload | null;
|
||||
featureFlags: null | FeatureFlagPayload;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user