Feature: SSO Login and Feature gating in UI (#1605)

* feat: added usefeatureflags hook and relevant code
* chore: resolved lint issues
* chore: applied translations
* feat: added signup for sso
This commit is contained in:
Amol Umbark
2022-10-04 13:43:58 +05:30
committed by GitHub
parent 9372f763c8
commit 106033c296
42 changed files with 922 additions and 127 deletions

View File

@@ -0,0 +1,13 @@
{
"column_license_key": "License Key",
"column_valid_from": "Valid From",
"column_valid_until": "Valid Until",
"column_license_status": "Status",
"button_apply": "Apply",
"placeholder_license_key": "Enter a License Key",
"tab_current_license": "Current License",
"tab_license_history": "History",
"loading_licenses": "Loading licenses...",
"enter_license_key": "Please enter a license key",
"license_applied": "License applied successfully, please refresh the page to see changes."
}

View File

@@ -0,0 +1,22 @@
{
"label_email": "Email",
"placeholder_email": "name@yourcompany.com",
"label_password": "Password",
"button_initiate_login": "Next",
"button_login": "Login",
"login_page_title": "Login with SigNoz",
"login_with_sso": "Login with SSO",
"login_with_pwd": "Login with password",
"forgot_password": "Forgot password?",
"create_an_account": "Create an account",
"prompt_if_admin": "If you are admin,",
"prompt_create_account": "If you are setting up SigNoz for the first time,",
"prompt_no_account": "Don't have an account? Contact your admin to send you an invite link.",
"prompt_forgot_password": "Ask your admin to reset your password and send you a new invite link",
"prompt_on_sso_error": "Are you trying to resolve SSO configuration issue?",
"unexpected_error": "Sorry, something went wrong",
"failed_to_login": "sorry, failed to login",
"invalid_email": "Please enter a valid email address",
"invalid_account": "This account does not exist. To create a new account, contact your admin to get an invite link",
"invalid_config": "Invalid configuration detected, please contact your administrator"
}

View File

@@ -0,0 +1,18 @@
{
"label_email": "Email",
"placeholder_email": "name@yourcompany.com",
"label_password": "Password",
"label_confirm_password": "Confirm Password",
"label_firstname": "First Name",
"placeholder_firstname": "Your Name",
"label_orgname": "Organization Name",
"placeholder_orgname": "Your Company",
"prompt_keepme_posted": "Keep me updated on new SigNoz features",
"prompt_anonymise": "Anonymise my usage date. We collect data to measure product usage",
"failed_confirm_password": "Passwords dont match. Please try again",
"unexpected_error": "Something went wrong",
"failed_to_initiate_login": "Signup completed but failed to initiate login",
"token_required": "Invite token is required for signup, please request one from your admin",
"button_get_started": "Get Started",
"prompt_admin_warning": "This will create an admin account. If you are not an admin, please ask your admin for an invite link"
}

View File

@@ -0,0 +1,13 @@
{
"column_license_key": "License Key",
"column_valid_from": "Valid From",
"column_valid_until": "Valid Until",
"column_license_status": "Status",
"button_apply": "Apply",
"placeholder_license_key": "Enter a License Key",
"tab_current_license": "Current License",
"tab_license_history": "History",
"loading_licenses": "Loading licenses...",
"enter_license_key": "Please enter a license key",
"license_applied": "License applied successfully, please refresh the page to see changes."
}

View File

@@ -0,0 +1,22 @@
{
"label_email": "Email",
"placeholder_email": "name@yourcompany.com",
"label_password": "Password",
"button_initiate_login": "Next",
"button_login": "Login",
"login_page_title": "Login with SigNoz",
"login_with_sso": "Login with SSO",
"login_with_pwd": "Login with password",
"forgot_password": "Forgot password?",
"create_an_account": "Create an account",
"prompt_if_admin": "If you are admin,",
"prompt_create_account": "If you are setting up SigNoz for the first time,",
"prompt_no_account": "Don't have an account? Contact your admin to send you an invite link.",
"prompt_forgot_password": "Ask your admin to reset your password and send you a new invite link",
"prompt_on_sso_error": "Are you trying to resolve SSO configuration issue?",
"unexpected_error": "Sorry, something went wrong",
"failed_to_login": "sorry, failed to login",
"invalid_email": "Please enter a valid email address",
"invalid_account": "This account does not exist. To create a new account, contact your admin to get an invite link",
"invalid_config": "Invalid configuration detected, please contact your administrator"
}

View File

@@ -0,0 +1,18 @@
{
"label_email": "Email",
"placeholder_email": "name@yourcompany.com",
"label_password": "Password",
"label_confirm_password": "Confirm Password",
"label_firstname": "First Name",
"placeholder_firstname": "Your Name",
"label_orgname": "Organization Name",
"placeholder_orgname": "Your Company",
"prompt_keepme_posted": "Keep me updated on new SigNoz features",
"prompt_anonymise": "Anonymise my usage date. We collect data to measure product usage",
"failed_confirm_password": "Passwords dont match. Please try again",
"unexpected_error": "Something went wrong",
"failed_to_initiate_login": "Signup completed but failed to initiate login",
"token_required": "Invite token is required for signup, please request one from your admin",
"button_get_started": "Get Started",
"prompt_admin_warning": "This will create an admin account. If you are not an admin, please ask your admin for an invite link"
}

View File

@@ -119,3 +119,7 @@ export const SomethingWentWrong = Loadable(
/* webpackChunkName: "SomethingWentWrong" */ 'pages/SomethingWentWrong'
),
);
export const LicensePage = Loadable(
() => import(/* webpackChunkName: "All Channels" */ 'pages/License'),
);

View File

@@ -12,6 +12,7 @@ import {
EditRulesPage,
ErrorDetails,
GettingStarted,
LicensePage,
ListAllALertsPage,
Login,
Logs,
@@ -166,6 +167,13 @@ const routes: AppRoutes[] = [
component: AllErrors,
key: 'ALL_ERROR',
},
{
path: ROUTES.LIST_LICENSES,
exact: true,
component: LicensePage,
isPrivate: true,
key: 'LIST_LICENSES',
},
{
path: ROUTES.ERROR_DETAIL,
exact: true,

View File

@@ -0,0 +1,23 @@
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/getFeatures';
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;

View File

@@ -79,6 +79,7 @@ const interceptorRejected = async (
// when refresh token is expired
if (response.status === 401 && response.config.url === '/login') {
console.log('logging out ');
Logout();
}
}

View File

@@ -0,0 +1,26 @@
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/licenses/apply';
const apply = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post('/licenses', {
key: props.key,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default apply;

View 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/licenses/getAll';
const getAll = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get('/licenses');
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getAll;

View File

@@ -8,7 +8,9 @@ const getInviteDetails = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(`/invite/${props.inviteId}`);
const response = await axios.get(
`/invite/${props.inviteId}?ref=${window.location.href}`,
);
return {
statusCode: 200,

View File

@@ -0,0 +1,28 @@
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/user/loginPrecheck';
const loginPrecheck = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.get(
`/loginPrecheck?email=${props.email}&ref=${encodeURIComponent(
window.location.href,
)}`,
);
return {
statusCode: 200,
error: null,
message: response.statusText,
payload: response.data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default loginPrecheck;

View File

@@ -2,21 +2,24 @@ import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import * as loginPrecheck from 'types/api/user/loginPrecheck';
import { Props } from 'types/api/user/signup';
const signup = async (
props: Props,
): Promise<SuccessResponse<string> | ErrorResponse> => {
): Promise<
SuccessResponse<null | loginPrecheck.PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.post(`/register`, {
...props,
});
console.log(' response.data.data', response.data.data);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
payload: response.data?.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);

View File

@@ -0,0 +1,6 @@
// keep this consistent with backend constants.go
export enum FeatureKeys {
SSO = 'SSO',
ENTERPRISE_PLAN = 'ENTERPRISE_PLAN',
BASIC_PLAN = 'BASIC_PLAN',
}

View File

@@ -29,6 +29,7 @@ const ROUTES = {
LOGS: '/logs',
HOME_PAGE: '/',
PASSWORD_RESET: '/password-reset',
LIST_LICENSES: '/licenses',
};
export default ROUTES;

View File

@@ -62,6 +62,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
if (getUserVersionResponse.status === 'idle' && isLoggedIn) {
getUserVersionResponse.refetch();
}
if (getFeaturesResponse.status === 'idle') {
getFeaturesResponse.refetch();
}
}, [
getFeaturesResponse,
getUserLatestVersionResponse,
@@ -112,6 +115,19 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
message: t('oops_something_went_wrong_version'),
});
}
if (
getFeaturesResponse.isFetched &&
getFeaturesResponse.isSuccess &&
getFeaturesResponse.data &&
getFeaturesResponse.data.payload
) {
dispatch({
type: UPDATE_FEATURE_FLAGS,
payload: {
...getFeaturesResponse.data.payload,
},
});
}
if (
getUserVersionResponse.isFetched &&

View File

@@ -0,0 +1,43 @@
import { Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import useFeatureFlags from 'hooks/useFeatureFlag';
import history from 'lib/history';
import React from 'react';
import {
FreePlanIcon,
ManageLicenseContainer,
ManageLicenseWrapper,
} from './styles';
function ManageLicense({ onToggle }: ManageLicenseProps): JSX.Element {
const isEnterprise = useFeatureFlags(FeatureKeys.ENTERPRISE_PLAN);
return (
<>
<Typography>SIGNOZ STATUS</Typography>
<ManageLicenseContainer>
<ManageLicenseWrapper>
<FreePlanIcon />
<Typography>{!isEnterprise ? 'Free Plan' : 'Enterprise Plan'} </Typography>
</ManageLicenseWrapper>
<Typography.Link
onClick={(): void => {
onToggle();
history.push(ROUTES.LIST_LICENSES);
}}
>
Manage Licenses
</Typography.Link>
</ManageLicenseContainer>
</>
);
}
interface ManageLicenseProps {
onToggle: VoidFunction;
}
export default ManageLicense;

View File

@@ -0,0 +1,19 @@
import { MinusSquareOutlined } from '@ant-design/icons';
import styled from 'styled-components';
export const ManageLicenseContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1rem;
`;
export const ManageLicenseWrapper = styled.div`
display: flex;
gap: 0.5rem;
align-items: center;
`;
export const FreePlanIcon = styled(MinusSquareOutlined)`
background-color: hsla(0, 0%, 100%, 0.3);
`;

View File

@@ -26,6 +26,7 @@ import AppActions from 'types/actions';
import AppReducer from 'types/reducer/app';
import CurrentOrganization from './CurrentOrganization';
import ManageLicense from './ManageLicense';
import SignedInAS from './SignedInAs';
import { Container, LogoutContainer, ToggleButton } from './styles';
@@ -71,6 +72,8 @@ function HeaderContainer({ toggleDarkMode }: Props): JSX.Element {
<Divider />
<CurrentOrganization onToggle={onArrowClickHandler} />
<Divider />
<ManageLicense onToggle={onArrowClickHandler} />
<Divider />
<LogoutContainer>
<LogoutOutlined />
<div

View File

@@ -0,0 +1,77 @@
import { Button, Input, notification } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import apply from 'api/licenses/apply';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ApplyForm, ApplyFormContainer, LicenseInput } from './applyFormStyles';
function ApplyLicenseForm(): JSX.Element {
const { t } = useTranslation(['licenses']);
const [key, setKey] = useState('');
const [loading, setLoading] = useState(false);
const onFinish = async (values: unknown | { key: string }): Promise<void> => {
const params = values as { key: string };
if (params.key === '' || !params.key) {
notification.error({
message: 'Error',
description: t('enter_license_key'),
});
return;
}
setLoading(true);
try {
const response = await apply({
key: params.key,
});
if (response.statusCode === 200) {
notification.success({
message: 'Success',
description: t('license_applied'),
});
} else {
notification.error({
message: 'Error',
description: response.error || t('unexpected_error'),
});
}
} catch (e) {
notification.error({
message: 'Error',
description: t('unexpected_error'),
});
}
setLoading(false);
};
return (
<ApplyFormContainer>
<ApplyForm layout="inline" onFinish={onFinish}>
<LicenseInput labelAlign="left" name="key">
<Input
onChange={(e): void => {
setKey(e.target.value as string);
}}
placeholder={t('placeholder_license_key')}
/>
</LicenseInput>
<FormItem>
<Button
loading={loading}
disabled={loading}
type="primary"
htmlType="submit"
>
{t('button_apply')}
</Button>
</FormItem>
</ApplyForm>
{key && <div style={{ paddingLeft: '0.5em', color: '#666' }}> {key}</div>}
</ApplyFormContainer>
);
}
export default ApplyLicenseForm;

View File

@@ -0,0 +1,42 @@
/* eslint-disable react/display-name */
import { Table } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { License } from 'types/api/licenses/def';
import { PayloadProps } from 'types/api/licenses/getAll';
function ListLicenses({ licenses }: ListLicensesProps): JSX.Element {
const { t } = useTranslation(['licenses']);
const columns: ColumnsType<License> = [
{
title: t('column_license_status'),
dataIndex: 'status',
key: 'status',
},
{
title: t('column_license_key'),
dataIndex: 'key',
key: 'key',
},
{
title: t('column_valid_from'),
dataIndex: 'ValidFrom',
key: 'valid from',
},
{
title: t('column_valid_until'),
dataIndex: 'ValidUntil',
key: 'valid until',
},
];
return <Table rowKey="id" dataSource={licenses} columns={columns} />;
}
interface ListLicensesProps {
licenses: PayloadProps;
}
export default ListLicenses;

View File

@@ -0,0 +1,26 @@
import { Form } from 'antd';
import FormItem from 'antd/lib/form/FormItem';
import styled from 'styled-components';
export const ApplyFormContainer = styled.div`
&&& {
padding-top: 1em;
padding-bottom: 1em;
}
`;
export const ApplyForm = styled(Form)`
&&& {
width: 100%;
}
`;
export const LicenseInput = styled(FormItem)`
width: 200px;
&:focus {
width: 350px;
input {
width: 350px;
}
}
`;

View File

@@ -0,0 +1,43 @@
import { Tabs, Typography } from 'antd';
import getAll from 'api/licenses/getAll';
import Spinner from 'components/Spinner';
import useFetch from 'hooks/useFetch';
import React from 'react';
import { useTranslation } from 'react-i18next';
import ApplyLicenseForm from './ApplyLicenseForm';
import ListLicenses from './ListLicenses';
const { TabPane } = Tabs;
function Licenses(): JSX.Element {
const { t } = useTranslation(['licenses']);
const { loading, payload, error, errorMessage } = useFetch(getAll);
if (error) {
return <Typography>{errorMessage}</Typography>;
}
if (loading || payload === undefined) {
return <Spinner tip={t('loading_licenses')} height="90vh" />;
}
return (
<Tabs destroyInactiveTabPane defaultActiveKey="licenses">
<TabPane tabKey="licenses" tab={t('tab_current_license')} key="licenses">
<ApplyLicenseForm />
<ListLicenses
licenses={payload ? payload.filter((l) => l.isCurrent === true) : []}
/>
</TabPane>
<TabPane tabKey="history" tab={t('tab_license_history')} key="history">
<ListLicenses
licenses={payload ? payload.filter((l) => l.isCurrent === false) : []}
/>
</TabPane>
</Tabs>
);
}
export default Licenses;

View File

@@ -1,19 +1,109 @@
import { Button, Input, notification, Space, Typography } from 'antd';
import { Button, Input, notification, Space, Tooltip, Typography } from 'antd';
import loginApi from 'api/user/login';
import loginPrecheckApi from 'api/user/loginPrecheck';
import afterLogin from 'AppRoutes/utils';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PayloadProps as PrecheckResultType } from 'types/api/user/loginPrecheck';
import { FormContainer, FormWrapper, Label, ParentContainer } from './styles';
const { Title } = Typography;
function Login(): JSX.Element {
interface LoginProps {
jwt: string;
refreshjwt: string;
userId: string;
ssoerror: string;
withPassword: string;
}
function Login({
jwt,
refreshjwt,
userId,
ssoerror = '',
withPassword = '0',
}: LoginProps): JSX.Element {
const { t } = useTranslation(['login']);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [email, setEmail] = useState<string>('');
const [password, setPassword] = useState<string>('');
const [precheckResult, setPrecheckResult] = useState<PrecheckResultType>({
sso: false,
ssoUrl: '',
canSelfRegister: false,
isUser: true,
});
const [precheckInProcess, setPrecheckInProcess] = useState(false);
const [precheckComplete, setPrecheckComplete] = useState(false);
useEffect(() => {
if (withPassword === 'Y') {
setPrecheckComplete(true);
}
}, [withPassword]);
useEffect(() => {
async function processJwt(): Promise<void> {
if (jwt && jwt !== '') {
setIsLoading(true);
await afterLogin(userId, jwt, refreshjwt);
setIsLoading(false);
history.push(ROUTES.APPLICATION);
}
}
processJwt();
}, [jwt, refreshjwt, userId]);
useEffect(() => {
if (ssoerror !== '') {
notification.error({
message: t('failed_to_login'),
});
}
}, [ssoerror, t]);
const onNextHandler = async (): Promise<void> => {
if (!email) {
notification.error({
message: t('invalid_email'),
});
return;
}
setPrecheckInProcess(true);
try {
const response = await loginPrecheckApi({
email,
});
if (response.statusCode === 200) {
setPrecheckResult({ ...precheckResult, ...response.payload });
const { isUser } = response.payload;
if (isUser) {
setPrecheckComplete(true);
} else {
notification.error({
message: t('invalid_account'),
});
}
} else {
notification.error({
message: t('invalid_config'),
});
}
} catch (e) {
console.log('failed to call precheck Api', e);
notification.error({ message: t('unexpected_error') });
}
setPrecheckInProcess(false);
};
const onChangeHandler = (
setFunc: React.Dispatch<React.SetStateAction<string>>,
value: string,
@@ -42,26 +132,53 @@ function Login(): JSX.Element {
history.push(ROUTES.APPLICATION);
} else {
notification.error({
message: response.error || 'Something went wrong',
message: response.error || t('unexpected_error'),
});
}
setIsLoading(false);
} catch (error) {
setIsLoading(false);
notification.error({
message: 'Something went wrong',
message: t('unexpected_error'),
});
}
};
const renderSAMLAction = (): JSX.Element => {
return (
<Button
type="primary"
loading={isLoading}
disabled={isLoading}
href={precheckResult.ssoUrl}
>
{t('login_with_sso')}
</Button>
);
};
const renderOnSsoError = (): JSX.Element | null => {
if (!ssoerror) {
return null;
}
return (
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
{t('prompt_on_sso_error')}{' '}
<a href="/login?password=Y">{t('login_with_pwd')}</a>.
</Typography.Paragraph>
);
};
const { sso, canSelfRegister } = precheckResult;
return (
<FormWrapper>
<FormContainer onSubmit={onSubmitHandler}>
<Title level={4}>Login to SigNoz</Title>
<Title level={4}>{t('login_page_title')}</Title>
<ParentContainer>
<Label htmlFor="signupEmail">Email</Label>
<Label htmlFor="signupEmail">{t('label_email')}</Label>
<Input
placeholder="name@yourcompany.com"
placeholder={t('placeholder_email')}
type="email"
autoFocus
required
@@ -71,46 +188,87 @@ function Login(): JSX.Element {
disabled={isLoading}
/>
</ParentContainer>
<ParentContainer>
<Label htmlFor="Password">Password</Label>
<Input.Password
required
id="currentPassword"
onChange={(event): void =>
onChangeHandler(setPassword, event.target.value)
}
disabled={isLoading}
value={password}
/>
</ParentContainer>
{precheckComplete && !sso && (
<ParentContainer>
<Label htmlFor="Password">{t('label_password')}</Label>
<Input.Password
required
id="currentPassword"
onChange={(event): void =>
onChangeHandler(setPassword, event.target.value)
}
disabled={isLoading}
value={password}
/>
<Tooltip title={t('prompt_forgot_password')}>
<Typography.Link>{t('forgot_password')}</Typography.Link>
</Tooltip>
</ParentContainer>
)}
<Space
style={{ marginTop: '1.3125rem' }}
align="start"
direction="vertical"
size={20}
>
<Button
disabled={isLoading}
loading={isLoading}
type="primary"
htmlType="submit"
data-attr="signup"
>
Login
</Button>
<Typography.Link
onClick={(): void => {
history.push(ROUTES.SIGN_UP);
}}
style={{ fontWeight: 700 }}
>
Create an account
</Typography.Link>
{!precheckComplete && (
<Button
disabled={precheckInProcess}
loading={precheckInProcess}
type="primary"
onClick={onNextHandler}
>
{t('button_initiate_login')}
</Button>
)}
{precheckComplete && !sso && (
<Button
disabled={isLoading}
loading={isLoading}
type="primary"
htmlType="submit"
data-attr="signup"
>
{t('button_login')}
</Button>
)}
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
If you have forgotten you password, ask your admin to reset password and
send you a new invite link
</Typography.Paragraph>
{precheckComplete && sso && renderSAMLAction()}
{!precheckComplete && ssoerror && renderOnSsoError()}
{!canSelfRegister && (
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
{t('prompt_no_account')}
</Typography.Paragraph>
)}
{!canSelfRegister && (
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
{t('prompt_create_account')}{' '}
<Typography.Link
onClick={(): void => {
history.push(ROUTES.SIGN_UP);
}}
style={{ fontWeight: 700 }}
>
{t('create_an_account')}
</Typography.Link>
</Typography.Paragraph>
)}
{canSelfRegister && (
<Typography.Paragraph italic style={{ color: '#ACACAC' }}>
{t('prompt_if_admin')}{' '}
<Typography.Link
onClick={(): void => {
history.push(ROUTES.SIGN_UP);
}}
style={{ fontWeight: 700 }}
>
{t('create_an_account')}
</Typography.Link>
</Typography.Paragraph>
)}
</Space>
</FormContainer>
</FormWrapper>

View File

@@ -4,9 +4,14 @@ import styled from 'styled-components';
export const FormWrapper = styled(Card)`
display: flex;
justify-content: center;
min-width: 390px;
min-height: 430px;
max-width: 432px;
flex: 1;
align-items: flex-start;
&&&.ant-card-body {
min-width: 100%;
}
`;
export const Label = styled.label`
@@ -21,6 +26,7 @@ export const FormContainer = styled.form`
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
`;
export const ParentContainer = styled.div`

View File

@@ -7,7 +7,6 @@ const useFeatureFlag = (flagKey: string): boolean => {
const { featureFlags } = useSelector<AppState, AppReducer>(
(state) => state.app,
);
return _get(featureFlags, flagKey, false);
};

View File

@@ -0,0 +1,8 @@
import Licenses from 'container/Licenses';
import React from 'react';
function LicensePage(): JSX.Element {
return <Licenses />;
}
export default LicensePage;

View File

@@ -3,6 +3,7 @@ import getUserVersion from 'api/user/getVersion';
import Spinner from 'components/Spinner';
import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
import LoginContainer from 'container/Login';
import useURLQuery from 'hooks/useUrlQuery';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
@@ -14,6 +15,13 @@ function Login(): JSX.Element {
const { isLoggedIn } = useSelector<AppState, AppReducer>((state) => state.app);
const { t } = useTranslation();
const urlQueryParams = useURLQuery();
const jwt = urlQueryParams.get('jwt') || '';
const refreshJwt = urlQueryParams.get('refreshjwt') || '';
const userId = urlQueryParams.get('usr') || '';
const ssoerror = urlQueryParams.get('ssoerror') || '';
const withPassword = urlQueryParams.get('password') || '';
const versionResult = useQuery({
queryFn: getUserVersion,
queryKey: 'getUserVersion',
@@ -42,7 +50,13 @@ function Login(): JSX.Element {
return (
<WelcomeLeftContainer version={version}>
<LoginContainer />
<LoginContainer
ssoerror={ssoerror}
jwt={jwt}
refreshjwt={refreshJwt}
userId={userId}
withPassword={withPassword}
/>
</WelcomeLeftContainer>
);
}

View File

@@ -8,10 +8,12 @@ import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'react-query';
import { useLocation } from 'react-router-dom';
import { SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getUser';
import * as loginPrecheck from 'types/api/user/loginPrecheck';
import { ButtonContainer, FormWrapper, Label, MarginTop } from './styles';
import { isPasswordNotValidMessage, isPasswordValid } from './utils';
@@ -19,8 +21,14 @@ import { isPasswordNotValidMessage, isPasswordValid } from './utils';
const { Title } = Typography;
function SignUp({ version }: SignUpProps): JSX.Element {
const { t } = useTranslation(['signup']);
const [loading, setLoading] = useState(false);
const [precheck, setPrecheck] = useState<loginPrecheck.PayloadProps>({
sso: false,
isUser: false,
});
const [firstName, setFirstName] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [organizationName, setOrganizationName] = useState<string>('');
@@ -54,12 +62,27 @@ function SignUp({ version }: SignUpProps): JSX.Element {
getInviteDetailsResponse.data.payload
) {
const responseDetails = getInviteDetailsResponse.data.payload;
if (responseDetails.precheck) setPrecheck(responseDetails.precheck);
setFirstName(responseDetails.name);
setEmail(responseDetails.email);
setOrganizationName(responseDetails.organization);
setIsDetailsDisable(true);
}
}, [getInviteDetailsResponse?.data?.payload, getInviteDetailsResponse.status]);
if (
getInviteDetailsResponse.status === 'success' &&
getInviteDetailsResponse.data?.error
) {
const { error } = getInviteDetailsResponse.data;
notification.error({
message: error,
});
}
}, [
getInviteDetailsResponse.data?.payload,
getInviteDetailsResponse.data?.error,
getInviteDetailsResponse.status,
getInviteDetailsResponse,
]);
const setState = (
value: string,
@@ -68,7 +91,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
setFunction(value);
};
const defaultError = 'Something went wrong';
const isPreferenceVisible = token === null;
const commonHandler = async (
@@ -101,17 +123,17 @@ function SignUp({ version }: SignUpProps): JSX.Element {
}
} else {
notification.error({
message: loginResponse.error || defaultError,
message: loginResponse.error || t('unexpected_error'),
});
}
} else {
notification.error({
message: response.error || defaultError,
message: response.error || t('unexpected_error'),
});
}
} catch (error) {
notification.error({
message: defaultError,
message: t('unexpected_error'),
});
}
};
@@ -129,10 +151,57 @@ function SignUp({ version }: SignUpProps): JSX.Element {
history.push(ROUTES.APPLICATION);
} else {
notification.error({
message: editResponse.error || defaultError,
message: editResponse.error || t('unexpected_error'),
});
}
};
const handleSubmitSSO = async (
e: React.FormEvent<HTMLFormElement>,
): Promise<void> => {
if (!params.get('token')) {
notification.error({
message: t('token_required'),
});
return;
}
setLoading(true);
try {
e.preventDefault();
const response = await signUpApi({
email,
name: firstName,
orgName: organizationName,
password,
token: params.get('token') || undefined,
sourceUrl: encodeURIComponent(window.location.href),
});
if (response.statusCode === 200) {
if (response.payload?.sso) {
if (response.payload?.ssoUrl) {
window.location.href = response.payload?.ssoUrl;
} else {
notification.error({
message: t('failed_to_initiate_login'),
});
// take user to login page as there is nothing to do here
history.push(ROUTES.LOGIN);
}
}
} else {
notification.error({
message: response.error || t('unexpected_error'),
});
}
} catch (error) {
notification.error({
message: t('unexpected_error'),
});
}
setLoading(false);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>): void => {
(async (): Promise<void> => {
@@ -159,7 +228,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
setLoading(false);
} catch (error) {
notification.error({
message: defaultError,
message: t('unexpected_error'),
});
setLoading(false);
}
@@ -195,12 +264,12 @@ function SignUp({ version }: SignUpProps): JSX.Element {
return (
<WelcomeLeftContainer version={version}>
<FormWrapper>
<form onSubmit={handleSubmit}>
<form onSubmit={!precheck.sso ? handleSubmit : handleSubmitSSO}>
<Title level={4}>Create your account</Title>
<div>
<Label htmlFor="signupEmail">Email</Label>
<Label htmlFor="signupEmail">{t('label_email')}</Label>
<Input
placeholder="name@yourcompany.com"
placeholder={t('placeholder_email')}
type="email"
autoFocus
value={email}
@@ -215,9 +284,9 @@ function SignUp({ version }: SignUpProps): JSX.Element {
{isNameVisible && (
<div>
<Label htmlFor="signupFirstName">First Name</Label>
<Label htmlFor="signupFirstName">{t('label_firstname')}</Label>
<Input
placeholder="Your Name"
placeholder={t('placeholder_firstname')}
value={firstName}
onChange={(e): void => {
setState(e.target.value, setFirstName);
@@ -230,9 +299,9 @@ function SignUp({ version }: SignUpProps): JSX.Element {
)}
<div>
<Label htmlFor="organizationName">Organization Name</Label>
<Label htmlFor="organizationName">{t('label_orgname')}</Label>
<Input
placeholder="Your Company"
placeholder={t('placeholder_orgname')}
value={organizationName}
onChange={(e): void => {
setState(e.target.value, setOrganizationName);
@@ -242,53 +311,57 @@ function SignUp({ version }: SignUpProps): JSX.Element {
disabled={isDetailsDisable}
/>
</div>
<div>
<Label htmlFor="Password">Password</Label>
<Input.Password
value={password}
onChange={(e): void => {
setState(e.target.value, setPassword);
}}
required
id="currentPassword"
/>
</div>
<div>
<Label htmlFor="ConfirmPassword">Confirm Password</Label>
<Input.Password
value={confirmPassword}
onChange={(e): void => {
const updateValue = e.target.value;
setState(updateValue, setConfirmPassword);
}}
required
id="confirmPassword"
/>
{!precheck.sso && (
<div>
<Label htmlFor="Password">{t('label_password')}</Label>
<Input.Password
value={password}
onChange={(e): void => {
setState(e.target.value, setPassword);
}}
required
id="currentPassword"
/>
</div>
)}
{!precheck.sso && (
<div>
<Label htmlFor="ConfirmPassword">{t('label_confirm_password')}</Label>
<Input.Password
value={confirmPassword}
onChange={(e): void => {
const updateValue = e.target.value;
setState(updateValue, setConfirmPassword);
}}
required
id="confirmPassword"
/>
{confirmPasswordError && (
<Typography.Paragraph
italic
id="password-confirm-error"
style={{
color: '#D89614',
marginTop: '0.50rem',
}}
>
Passwords dont match. Please try again
</Typography.Paragraph>
)}
{isPasswordPolicyError && (
<Typography.Paragraph
italic
style={{
color: '#D89614',
marginTop: '0.50rem',
}}
>
{isPasswordNotValidMessage}
</Typography.Paragraph>
)}
</div>
{confirmPasswordError && (
<Typography.Paragraph
italic
id="password-confirm-error"
style={{
color: '#D89614',
marginTop: '0.50rem',
}}
>
{t('failed_confirm_password')}
</Typography.Paragraph>
)}
{isPasswordPolicyError && (
<Typography.Paragraph
italic
style={{
color: '#D89614',
marginTop: '0.50rem',
}}
>
{isPasswordNotValidMessage}
</Typography.Paragraph>
)}
</div>
)}
{isPreferenceVisible && (
<>
@@ -298,7 +371,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
onChange={(value): void => onSwitchHandler(value, setHasOptedUpdates)}
checked={hasOptedUpdates}
/>
<Typography>Keep me updated on new SigNoz features</Typography>
<Typography>{t('prompt_keepme_posted')} </Typography>
</Space>
</MarginTop>
@@ -308,9 +381,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
onChange={(value): void => onSwitchHandler(value, setIsAnonymous)}
checked={isAnonymous}
/>
<Typography>
Anonymise my usage date. We collect data to measure product usage
</Typography>
<Typography>{t('prompt_anonymise')}</Typography>
</Space>
</MarginTop>
</>
@@ -339,14 +410,13 @@ function SignUp({ version }: SignUpProps): JSX.Element {
loading ||
!email ||
!organizationName ||
!password ||
!confirmPassword ||
(!precheck.sso && (!password || !confirmPassword)) ||
!firstName ||
confirmPasswordError ||
isPasswordPolicyError
}
>
Get Started
{t('button_get_started')}
</Button>
</ButtonContainer>
</form>

View File

@@ -48,6 +48,7 @@ const InitialValue: InitialValueTypes = {
isSideBarCollapsed: getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
currentVersion: '',
latestVersion: '',
featureFlags: {},
isCurrentVersionError: false,
isLatestVersionError: false,
user: getInitialUser(),
@@ -55,7 +56,6 @@ const InitialValue: InitialValueTypes = {
isUserFetchingError: false,
org: null,
role: null,
featureFlags: null,
};
const appReducer = (
@@ -84,6 +84,13 @@ const appReducer = (
};
}
case UPDATE_FEATURE_FLAGS: {
return {
...state,
featureFlags: { ...action.payload },
};
}
case UPDATE_CURRENT_VERSION: {
return {
...state,
@@ -196,13 +203,6 @@ const appReducer = (
};
}
case UPDATE_FEATURE_FLAGS: {
return {
...state,
featureFlags: action.payload,
};
}
case UPDATE_ORG: {
return {
...state,

View File

@@ -40,6 +40,10 @@ export interface SideBarCollapse {
payload: boolean;
}
export interface UpdateFeatureFlags {
type: typeof UPDATE_FEATURE_FLAGS;
payload: null | FeatureFlagPayload;
}
export interface UpdateAppVersion {
type: typeof UPDATE_CURRENT_VERSION;
payload: {
@@ -112,11 +116,6 @@ export interface UpdateOrg {
};
}
export interface UpdateFeatureFlags {
type: typeof UPDATE_FEATURE_FLAGS;
payload: FeatureFlagPayload;
}
export type AppAction =
| SwitchDarkMode
| LoggedInUser

View File

@@ -0,0 +1,3 @@
export interface PayloadProps {
[key: string]: boolean;
}

View File

@@ -0,0 +1,10 @@
import { License } from './def';
export interface Props {
key: string;
}
export interface PayloadProps {
status: string;
data: License;
}

View File

@@ -0,0 +1,8 @@
export interface License {
key: string;
ValidFrom: Date;
ValidUntil: Date;
planKey: string;
status: string;
isCurrent: boolean;
}

View File

@@ -0,0 +1,3 @@
import { License } from './def';
export type PayloadProps = License[];

View File

@@ -2,6 +2,7 @@ import { User } from 'types/reducer/app';
import { ROLES } from 'types/roles';
import { Organization } from './getOrganization';
import * as loginPrecheck from './loginPrecheck';
export interface Props {
inviteId: string;
@@ -14,4 +15,5 @@ export interface PayloadProps {
role: ROLES;
token: string;
organization: Organization['name'];
precheck?: loginPrecheck.PayloadProps;
}

View File

@@ -1,3 +1,4 @@
export interface PayloadProps {
version: string;
ee: string;
}

View File

@@ -0,0 +1,11 @@
export interface PayloadProps {
sso: boolean;
ssoUrl?: string;
canSelfRegister?: boolean;
isUser: boolean;
}
export interface Props {
email?: string;
path?: string;
}

View File

@@ -4,4 +4,5 @@ export interface Props {
email: string;
password: string;
token?: string;
sourceUrl?: string;
}

View File

@@ -69,4 +69,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
USAGE_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
VERSION: ['ADMIN', 'EDITOR', 'VIEWER'],
LOGS: ['ADMIN', 'EDITOR', 'VIEWER'],
LIST_LICENSES: ['ADMIN'],
};