diff --git a/client/packages/lowcoder-design/src/components/iconSelect/index.tsx b/client/packages/lowcoder-design/src/components/iconSelect/index.tsx index 14b1f577f..85643d344 100644 --- a/client/packages/lowcoder-design/src/components/iconSelect/index.tsx +++ b/client/packages/lowcoder-design/src/components/iconSelect/index.tsx @@ -398,7 +398,7 @@ export const IconSelect = (props: { visible={visible} setVisible={setVisible} trigger="click" - leftOffset={-96} + leftOffset={-30} searchKeywords={props.searchKeywords} /> ); diff --git a/client/packages/lowcoder/src/comps/controls/iconControl.tsx b/client/packages/lowcoder/src/comps/controls/iconControl.tsx index 51311e1b5..15b5f9dc2 100644 --- a/client/packages/lowcoder/src/comps/controls/iconControl.tsx +++ b/client/packages/lowcoder/src/comps/controls/iconControl.tsx @@ -70,7 +70,7 @@ const Wrapper = styled.div` } `; -const IconPicker = (props: { +export const IconPicker = (props: { value: string; onChange: (value: string) => void; label?: ReactNode; diff --git a/client/packages/lowcoder/src/comps/generators/uiCompBuilder.tsx b/client/packages/lowcoder/src/comps/generators/uiCompBuilder.tsx index c3c189c27..f5808ef38 100644 --- a/client/packages/lowcoder/src/comps/generators/uiCompBuilder.tsx +++ b/client/packages/lowcoder/src/comps/generators/uiCompBuilder.tsx @@ -54,22 +54,6 @@ export function HidableView(props: { } } -export function ExtendedComponentView(props: { - children: JSX.Element | React.ReactNode; - className: string; - dataTestId: string; -}) { - if (!props.className && !props.dataTestId) { - return <>{props.children}; - } - - return ( -
- {props.children} -
- ); -} - export function ExtendedPropertyView< ChildrenCompMap extends Record>, >(props: { @@ -196,7 +180,13 @@ export class UICompBuilder< } override getView(): ViewReturn { - return (
); + return ( + + ); } override getPropertyView(): ReactNode { @@ -223,7 +213,11 @@ export const DisabledContext = React.createContext(false); /** * Guaranteed to be in a react component, so that react hooks can be used internally */ -function UIView(props: { comp: any; viewFn: any }) { +function UIView(props: { + innerRef: React.RefObject; + comp: any; + viewFn: any; +}) { const comp = props.comp; const childrenProps = childrenToProps(comp.children); @@ -243,13 +237,22 @@ function UIView(props: { comp: any; viewFn: any }) { //END ADD BY FRED return ( - + data-testid={childrenProps.dataTestId as string} + style={{ + width: "100%", + height: "100%", + margin: "0px", + padding: "0px", + }}> - + ); } diff --git a/client/packages/lowcoder/src/constants/authConstants.ts b/client/packages/lowcoder/src/constants/authConstants.ts index 0207d6abf..6c19a0bf8 100644 --- a/client/packages/lowcoder/src/constants/authConstants.ts +++ b/client/packages/lowcoder/src/constants/authConstants.ts @@ -90,9 +90,9 @@ export const AuthRoutes: Array<{ path: string; component: React.ComponentType void; + onCancel: () => void; +}; + +function GeneralOAuthForm(props: GeneralOAuthFormProp) { + const { + authType, + onSave, + onCancel, + } = props; + const [form1] = Form.useForm(); + const [saveLoading, setSaveLoading] = useState(false); + + function saveAuthProvider(values: ConfigItem) { + setSaveLoading(true); + const config = { + ...values, + authType, + enableRegister: true, + } + IdSourceApi.saveConfig(config) + .then((resp) => { + if (validateResponse(resp)) { + messageInstance.success(trans("idSource.saveSuccess")); + } + }) + .catch((e) => messageInstance.error(e.message)) + .finally(() => { + setSaveLoading(false); + onSave(); + }); + } + + const handleSave = () => { + form1.validateFields().then(values => { + console.log(values); + saveAuthProvider(values); + }); + } + + function handleCancel() { + onCancel(); + } + + const authConfigForm = useMemo(() => { + if(!authConfig[authType]) return clientIdandSecretConfig; + return authConfig[authType].form; + }, [authType]) + + return ( + + {Object.entries(authConfigForm).map(([key, value]) => { + const valueObject = _.isObject(value) ? (value as ItemType) : false; + const required = true; + const label = valueObject ? valueObject.label : value; + const tip = valueObject && valueObject.tip; + const isPassword = valueObject && valueObject.isPassword; + return ( +
+ + {label}: + + + ) : ( + + {label}: + + ) + } + > + {isPassword ? ( + + ) : ( + + )} + +
+ ); + })} + + + + +
+ ); +} + +export default GeneralOAuthForm; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/OAuthForms/GenericOAuthForm.tsx b/client/packages/lowcoder/src/pages/setting/idSource/OAuthForms/GenericOAuthForm.tsx new file mode 100644 index 000000000..f88ec0628 --- /dev/null +++ b/client/packages/lowcoder/src/pages/setting/idSource/OAuthForms/GenericOAuthForm.tsx @@ -0,0 +1,314 @@ +import { useState } from "react"; +import { messageInstance, CloseEyeIcon } from "lowcoder-design"; +import { i18nObjs, trans } from "i18n"; +import { + FormStyled, + PasswordLabel, + StyledSteps +} from "../styledComponents"; +import { default as Form, FormInstance } from "antd/es/form"; +import { default as Input } from "antd/es/input"; +import { default as Tooltip } from "antd/es/tooltip"; +import IdSourceApi, { ConfigItem } from "api/idSourceApi"; +import { validateResponse } from "api/apiUtils"; +import { authConfig, AuthType, ItemType } from "../idSourceConstants"; +import _ from "lodash"; +import Flex from "antd/es/flex"; +import Button from "antd/es/button"; +import axios from "axios"; +import { IconPicker } from "@lowcoder-ee/comps/controls/iconControl"; + +const sourceMappingKeys = [ + 'uid', + 'email', + 'username', + 'avatar', +]; + +const steps = [ + { + title: 'Step 1', + description: 'Issuer endpoint', + }, + { + title: 'Step 2', + description: 'Provider details', + }, + { + title: 'Step 3', + description: 'Mapping', + }, +]; + +interface OpenIdProvider { + issuer: string, + authorization_endpoint: string, + token_endpoint: string, + userinfo_endpoint: string, + jwks_uri?: string, + scopes_supported: string[], +} + +interface ConfigProvider { + authType: string, + source: string, + sourceName: string, + sourceDescription?: string, + sourceIcon?: string, + sourceCategory?: string, + issuer: string, + authorizationEndpoint: string, + tokenEndpoint: string, + userInfoEndpoint: string, + jwksUri?: string, + scope: string, + sourceMappings: any, +} + +type GenericOAuthFormProp = { + authType: AuthType, + onSave: () => void; + onCancel: () => void; +}; + +function GenericOAuthForm(props: GenericOAuthFormProp) { + const { + authType, + onSave, + onCancel + } = props; + const [form1] = Form.useForm(); + + const [saveLoading, setSaveLoading] = useState(false); + const [currentStep, setCurrentStep] = useState(0); + const [issuerDetails, setIssuerDetails] = useState({}); + + function saveAuthProvider(values: ConfigItem) { + setSaveLoading(true); + const config = { + ...values, + enableRegister: true, + } + IdSourceApi.saveConfig(config) + .then((resp) => { + if (validateResponse(resp)) { + messageInstance.success(trans("idSource.saveSuccess")); + onSave(); + } + }) + .catch((e) => messageInstance.error(e.message)) + .finally(() => { + setSaveLoading(false); + }); + } + + const handleStep1Save = () => { + form1.validateFields().then(async (values) => { + setSaveLoading(true); + const { issuer } = values; + try { + const res = await axios.get(`${issuer}/.well-known/openid-configuration`); + setIssuerDetails(() => { + const issuer = { + authType: AuthType.Generic, + source: '', + sourceName: '', + issuer: res.data.issuer, + authorizationEndpoint: res.data.authorization_endpoint, + tokenEndpoint: res.data.token_endpoint, + userInfoEndpoint: res.data.userinfo_endpoint, + jwksUri: res.data.jwks_uri, + scope: res.data.scopes_supported.join(','), + sourceMappings: sourceMappingKeys.map(sourceKey => ({ + [sourceKey]: sourceKey, + })) + }; + form1.setFieldsValue(issuer); + return issuer; + }) + } catch (e) { + setIssuerDetails({}); + } finally { + setSaveLoading(false); + setCurrentStep(currentStep => currentStep + 1); + } + }) + } + + const handleStep2Save = () => { + form1.validateFields().then(values => { + setIssuerDetails(issuerDetails => ({ + ...issuerDetails, + ...values, + })) + setCurrentStep(currentStep => currentStep + 1); + }) + } + + const handleStep3Save = () => { + form1.validateFields().then(values => { + setIssuerDetails((issuerDetails: any) => { + const updatedDetails = { + ...issuerDetails, + sourceMappings: { + ...values, + } + }; + saveAuthProvider(updatedDetails); + console.log('save details', updatedDetails); + return updatedDetails; + }); + }) + } + + const handleSave = () => { + if (currentStep === 0) { + return handleStep1Save(); + } + if (currentStep === 1) { + return handleStep2Save(); + } + if (currentStep === 2) { + return handleStep3Save(); + } + } + + function handleCancel() { + if (currentStep === 0) { + onCancel(); + return form1.resetFields(); + } + setCurrentStep(currentStep => currentStep - 1); + } + + const authConfigForm = authConfig[AuthType.Generic].form; + + return ( + <> + + + + {currentStep === 0 && ( + + + + )} + {currentStep === 1 && Object.entries(authConfigForm).map(([key, value]) => { + const valueObject = _.isObject(value) ? (value as ItemType) : false; + const required = true; + const label = valueObject ? valueObject.label : value as string; + const tip = valueObject && valueObject.tip; + const isPassword = valueObject && valueObject.isPassword; + const isIcon = valueObject && valueObject.isIcon; + return ( +
+ + {label}: + + + ) : ( + + {label}: + + ) + } + > + {isPassword ? ( + + ) : isIcon ? ( + form1.setFieldValue("sourceIcon", value)} + label={'Source Icon'} + value={form1.getFieldValue('sourceIcon')} + /> + ) : ( + + )} + +
+ ); + })} + {currentStep === 2 && sourceMappingKeys.map(sourceKey => ( + + + + + + + + ))} + + + + +
+ + ); +} + +export default GenericOAuthForm; diff --git a/client/packages/lowcoder/src/pages/setting/idSource/createModal.tsx b/client/packages/lowcoder/src/pages/setting/idSource/createModal.tsx index 8b2106568..d154bfec0 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/createModal.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/createModal.tsx @@ -1,25 +1,20 @@ -import { useEffect, useMemo, useState } from "react"; -import { messageInstance, CustomSelect, CloseEyeIcon } from "lowcoder-design"; +import { CustomSelect } from "lowcoder-design"; import { CustomModalStyled, } from "../styled"; import { trans } from "i18n"; import { FormStyled, - CheckboxStyled, SpanStyled, - PasswordLabel } from "./styledComponents"; import { default as Form } from "antd/es/form"; -import { default as Input } from "antd/es/input"; import { default as Select } from "antd/es/select"; -import { default as Tooltip } from "antd/es/tooltip"; -import IdSourceApi, { ConfigItem } from "api/idSourceApi"; -import { validateResponse } from "api/apiUtils"; -import { authConfig, AuthType, clientIdandSecretConfig, ItemType } from "./idSourceConstants"; +import { authConfig, AuthType } from "./idSourceConstants"; import { ServerAuthTypeInfo } from "constants/authConstants"; import { GeneralLoginIcon } from "assets/icons"; import _ from "lodash"; +import GenericOAuthForm from "./OAuthForms/GenericOAuthForm"; +import GeneralOAuthForm from "./OAuthForms/GeneralOAuthForm"; type CreateModalProp = { modalVisible: boolean; @@ -36,31 +31,9 @@ function CreateModal(props: CreateModalProp) { onConfigCreate } = props; const [form] = Form.useForm(); - const [saveLoading, setSaveLoading] = useState(false); - const handleOk = () => { - form.validateFields().then(values => { - // console.log(values) - saveAuthProvider(values) - }) - } - function saveAuthProvider(values: ConfigItem) { - setSaveLoading(true); - const config = { - ...values, - enableRegister: true, - } - IdSourceApi.saveConfig(config) - .then((resp) => { - if (validateResponse(resp)) { - messageInstance.success(trans("idSource.saveSuccess")); - } - }) - .catch((e) => messageInstance.error(e.message)) - .finally(() => { - setSaveLoading(false); - onConfigCreate(); - }); + const handleSave = () => { + onConfigCreate(); } function handleCancel() { @@ -71,6 +44,7 @@ function CreateModal(props: CreateModalProp) { const authConfigOptions = Object.values(authConfig) .filter(config => { return !(oauthProvidersList.indexOf(config.sourceValue) > -1) + || config.sourceValue === AuthType.Generic }) .map(config => ({ label: config.sourceName, @@ -79,22 +53,14 @@ function CreateModal(props: CreateModalProp) { const selectedAuthType = Form.useWatch('authType', form);; - const authConfigForm = useMemo(() => { - if(!authConfig[selectedAuthType]) return clientIdandSecretConfig; - return authConfig[selectedAuthType].form; - }, [selectedAuthType]) - return ( form.resetFields()} > @@ -102,7 +68,7 @@ function CreateModal(props: CreateModalProp) { form={form} name="basic" layout="vertical" - style={{ maxWidth: 440 }} + style={{ maxWidth: '100%' }} autoComplete="off" > - {Object.entries(authConfigForm).map(([key, value]) => { - const valueObject = _.isObject(value) ? (value as ItemType) : false; - const required = true; - const label = valueObject ? valueObject.label : value; - const tip = valueObject && valueObject.tip; - const isPassword = valueObject && valueObject.isPassword; - return ( -
- - {label}: - - - ) : ( - - {label}: - - ) - } - > - {isPassword ? ( - - ) : ( - - )} - -
- ); - })} + + {selectedAuthType === AuthType.Generic && ( + + )} + + {selectedAuthType !== AuthType.Generic && ( + + )}
); } diff --git a/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx b/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx index 291797178..6fa5cc34e 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/detail/index.tsx @@ -37,6 +37,7 @@ import { validateResponse } from "api/apiUtils"; import { ItemType } from "pages/setting/idSource/idSourceConstants"; import _ from "lodash"; import { messageInstance } from "lowcoder-design/src/components/GlobalInstances"; +import { IconPicker } from "@lowcoder-ee/comps/controls/iconControl"; type IdSourceDetailProps = { location: Location & { state: ConfigItem }; @@ -163,6 +164,7 @@ export const IdSourceDetail = (props: IdSourceDetailProps) => { const label = valueObject ? valueObject.label : value as string; const isList = valueObject && valueObject.isList; const isPassword = valueObject && valueObject.isPassword; + const isIcon = valueObject && valueObject.isIcon; return (
{ } autoComplete={"one-time-code"} /> - ) : !isPassword && !isList ? ( + ) : !isPassword && !isList && !isIcon ? ( { (lock ? handleLockClick()} /> : ) } /> + ) : !isPassword && !isList && isIcon ? ( + form.setFieldValue("sourceIcon", value)} + label={'Source Icon'} + value={form.getFieldValue('sourceIcon')} + /> ) : ( { return !FreeTypes.includes(type); @@ -83,6 +105,7 @@ export type ItemType = { isList?: boolean; isRequire?: boolean; isPassword?: boolean; + isIcon?: boolean; hasLock?: boolean; tip?: string; } diff --git a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx index 14fd3a43a..87a1071ca 100644 --- a/client/packages/lowcoder/src/pages/setting/idSource/list.tsx +++ b/client/packages/lowcoder/src/pages/setting/idSource/list.tsx @@ -91,18 +91,20 @@ export const IdSourceList = (props: any) => { <> - {trans("idSource.title")} - {currentOrgAdmin(user) && ( - } - onClick={() => - setModalVisible(true) - } - > - {"Add OAuth Provider"} - - )} + <> + {trans("idSource.title")} + {currentOrgAdmin(user) && ( + } + onClick={() => + setModalVisible(true) + } + > + {"Add OAuth Provider"} + + )} + { title={trans("idSource.loginType")} dataIndex="authType" key="authType" - render={(value, record: ConfigItem) => ( + render={(value: AuthType, record: ConfigItem) => ( { { alt={value} /> } - {authConfig[value as AuthType].sourceName} + + {value === AuthType.Generic + ? record.sourceName + : authConfig[value as AuthType].sourceName + } + {!FreeTypes.includes(value) && ( .ant-btn-loading-icon .anticon { @@ -62,8 +63,8 @@ export const SpanStyled = styled.div<{ disabled: boolean }>` height: 100%; img { - width: 32px; - height: 32px; + width: 25px; + height: 25px; margin-right: 12px; opacity: ${(props) => props.disabled && "0.4"}; } @@ -328,3 +329,18 @@ export const CreateButton = styled(Button)` height: 12px; } `; + +export const StyledSteps = styled(Steps)` + .ant-steps-item-icon { + width: 25px; + height: 25px; + font-size: 10px; + line-height: 26px; + } + + .ant-steps-item-title { + font-size: 14px; + line-height: 22px; + font-weight: bold; + } +`; \ No newline at end of file diff --git a/client/packages/lowcoder/src/pages/setting/styled.tsx b/client/packages/lowcoder/src/pages/setting/styled.tsx index 06f951857..72cc4f5e7 100644 --- a/client/packages/lowcoder/src/pages/setting/styled.tsx +++ b/client/packages/lowcoder/src/pages/setting/styled.tsx @@ -69,9 +69,9 @@ export const ModalNameDiv = styled.div` `; export const CustomModalStyled = styled(CustomModal)` - button { - margin-top: 20px; - } + // button { + // margin-top: 20px; + // } `; export const TacoInputStyled = styled(TacoInput)`