mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-02 02:00:41 +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",
|
"add_another_team_member": "Add another team member",
|
||||||
"invite_team_members": "Invite team members",
|
"invite_team_members": "Invite team members",
|
||||||
"invite_members": "Invite 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",
|
"add_another_team_member": "Add another team member",
|
||||||
"invite_team_members": "Invite team members",
|
"invite_team_members": "Invite team members",
|
||||||
"invite_members": "Invite 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 { notification } from 'antd';
|
||||||
|
import getFeaturesFlags from 'api/features/getFeatureFlags';
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import Header from 'container/Header';
|
import Header from 'container/Header';
|
||||||
@ -15,6 +16,7 @@ import AppActions from 'types/actions';
|
|||||||
import {
|
import {
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
|
UPDATE_FEATURE_FLAGS,
|
||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
UPDATE_LATEST_VERSION_ERROR,
|
UPDATE_LATEST_VERSION_ERROR,
|
||||||
} from 'types/actions/app';
|
} from 'types/actions/app';
|
||||||
@ -27,7 +29,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
const [
|
||||||
|
getUserVersionResponse,
|
||||||
|
getUserLatestVersionResponse,
|
||||||
|
getFeaturesResponse,
|
||||||
|
] = useQueries([
|
||||||
{
|
{
|
||||||
queryFn: getUserVersion,
|
queryFn: getUserVersion,
|
||||||
queryKey: 'getUserVersion',
|
queryKey: 'getUserVersion',
|
||||||
@ -38,9 +44,17 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
queryKey: 'getUserLatestVersion',
|
queryKey: 'getUserLatestVersion',
|
||||||
enabled: isLoggedIn,
|
enabled: isLoggedIn,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
queryFn: getFeaturesFlags,
|
||||||
|
queryKey: 'getFeatureFlags',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (getFeaturesResponse.status === 'idle') {
|
||||||
|
getFeaturesResponse.refetch();
|
||||||
|
}
|
||||||
|
|
||||||
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
if (getUserLatestVersionResponse.status === 'idle' && isLoggedIn) {
|
||||||
getUserLatestVersionResponse.refetch();
|
getUserLatestVersionResponse.refetch();
|
||||||
}
|
}
|
||||||
@ -48,7 +62,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
|
||||||
getUserVersionResponse.refetch();
|
getUserVersionResponse.refetch();
|
||||||
}
|
}
|
||||||
}, [getUserLatestVersionResponse, getUserVersionResponse, isLoggedIn]);
|
}, [
|
||||||
|
getFeaturesResponse,
|
||||||
|
getUserLatestVersionResponse,
|
||||||
|
getUserVersionResponse,
|
||||||
|
isLoggedIn,
|
||||||
|
]);
|
||||||
|
|
||||||
const { children } = props;
|
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,
|
dispatch,
|
||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
@ -135,6 +168,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
getUserLatestVersionResponse.isFetched,
|
getUserLatestVersionResponse.isFetched,
|
||||||
getUserVersionResponse.isFetched,
|
getUserVersionResponse.isFetched,
|
||||||
getUserLatestVersionResponse.isSuccess,
|
getUserLatestVersionResponse.isSuccess,
|
||||||
|
getFeaturesResponse.isFetched,
|
||||||
|
getFeaturesResponse.isSuccess,
|
||||||
|
getFeaturesResponse.data,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isToDisplayLayout = isLoggedIn;
|
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 { Divider, Space } from 'antd';
|
||||||
|
import { FeatureKeys } from 'constants/featureKeys';
|
||||||
|
import useFeatureFlag from 'hooks/useFeatureFlag';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
|
||||||
|
import AuthDomains from './AuthDomains';
|
||||||
import DisplayName from './DisplayName';
|
import DisplayName from './DisplayName';
|
||||||
import Members from './Members';
|
import Members from './Members';
|
||||||
import PendingInvitesContainer from './PendingInvitesContainer';
|
import PendingInvitesContainer from './PendingInvitesContainer';
|
||||||
@ -11,6 +14,9 @@ import PendingInvitesContainer from './PendingInvitesContainer';
|
|||||||
function OrganizationSettings(): JSX.Element {
|
function OrganizationSettings(): JSX.Element {
|
||||||
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
const { org } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||||
|
|
||||||
|
const sso = useFeatureFlag(FeatureKeys.SSO);
|
||||||
|
const noUpsell = useFeatureFlag(FeatureKeys.DISABLE_UPSELL);
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
@ -31,6 +37,8 @@ function OrganizationSettings(): JSX.Element {
|
|||||||
<PendingInvitesContainer />
|
<PendingInvitesContainer />
|
||||||
<Divider />
|
<Divider />
|
||||||
<Members />
|
<Members />
|
||||||
|
<Divider />
|
||||||
|
{(!noUpsell || (noUpsell && sso)) && <AuthDomains />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { StyledDiv } from 'components/Styled';
|
|||||||
import { ITraceMetaData } from 'container/GantChart';
|
import { ITraceMetaData } from 'container/GantChart';
|
||||||
import { IIntervalUnit, INTERVAL_UNITS } from 'container/TraceDetail/utils';
|
import { IIntervalUnit, INTERVAL_UNITS } from 'container/TraceDetail/utils';
|
||||||
import useThemeMode from 'hooks/useThemeMode';
|
import useThemeMode from 'hooks/useThemeMode';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { styles, Svg, TimelineInterval } from './styles';
|
import { styles, Svg, TimelineInterval } from './styles';
|
||||||
@ -20,10 +20,6 @@ function Timeline({
|
|||||||
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
const [ref, { width }] = useMeasure<HTMLDivElement>();
|
||||||
const { isDarkMode } = useThemeMode();
|
const { isDarkMode } = useThemeMode();
|
||||||
|
|
||||||
const asd = useRef('');
|
|
||||||
|
|
||||||
asd.current = '1';
|
|
||||||
|
|
||||||
const [intervals, setIntervals] = useState<Interval[] | null>(null);
|
const [intervals, setIntervals] = useState<Interval[] | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
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,
|
SWITCH_DARK_MODE,
|
||||||
UPDATE_CURRENT_ERROR,
|
UPDATE_CURRENT_ERROR,
|
||||||
UPDATE_CURRENT_VERSION,
|
UPDATE_CURRENT_VERSION,
|
||||||
|
UPDATE_FEATURE_FLAGS,
|
||||||
UPDATE_LATEST_VERSION,
|
UPDATE_LATEST_VERSION,
|
||||||
UPDATE_LATEST_VERSION_ERROR,
|
UPDATE_LATEST_VERSION_ERROR,
|
||||||
UPDATE_ORG,
|
UPDATE_ORG,
|
||||||
@ -54,6 +55,7 @@ const InitialValue: InitialValueTypes = {
|
|||||||
isUserFetchingError: false,
|
isUserFetchingError: false,
|
||||||
org: null,
|
org: null,
|
||||||
role: null,
|
role: null,
|
||||||
|
featureFlags: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const appReducer = (
|
const appReducer = (
|
||||||
@ -194,6 +196,13 @@ const appReducer = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case UPDATE_FEATURE_FLAGS: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
featureFlags: action.payload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case UPDATE_ORG: {
|
case UPDATE_ORG: {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { PayloadProps as FeatureFlagPayload } from 'types/api/features/getFeaturesFlags';
|
||||||
import {
|
import {
|
||||||
Organization,
|
Organization,
|
||||||
PayloadProps as OrgPayload,
|
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_USER = 'UPDATE_USER';
|
||||||
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
export const UPDATE_ORG_NAME = 'UPDATE_ORG_NAME';
|
||||||
export const UPDATE_ORG = 'UPDATE_ORG';
|
export const UPDATE_ORG = 'UPDATE_ORG';
|
||||||
|
export const UPDATE_FEATURE_FLAGS = 'UPDATE_FEATURE_FLAGS';
|
||||||
|
|
||||||
export interface SwitchDarkMode {
|
export interface SwitchDarkMode {
|
||||||
type: typeof SWITCH_DARK_MODE;
|
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 =
|
export type AppAction =
|
||||||
| SwitchDarkMode
|
| SwitchDarkMode
|
||||||
| LoggedInUser
|
| LoggedInUser
|
||||||
@ -122,4 +129,5 @@ export type AppAction =
|
|||||||
| UpdateUserOrgRole
|
| UpdateUserOrgRole
|
||||||
| UpdateUser
|
| UpdateUser
|
||||||
| UpdateOrgName
|
| 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 OrgPayload } from 'types/api/user/getOrganization';
|
||||||
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
import { PayloadProps as UserPayload } from 'types/api/user/getUser';
|
||||||
import { ROLES } from 'types/roles';
|
import { ROLES } from 'types/roles';
|
||||||
@ -24,4 +25,5 @@ export default interface AppReducer {
|
|||||||
isUserFetchingError: boolean;
|
isUserFetchingError: boolean;
|
||||||
role: ROLES | null;
|
role: ROLES | null;
|
||||||
org: OrgPayload | null;
|
org: OrgPayload | null;
|
||||||
|
featureFlags: null | FeatureFlagPayload;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user