diff --git a/.changeset/fast-rules-complain.md b/.changeset/fast-rules-complain.md new file mode 100644 index 00000000000..24f77932be3 --- /dev/null +++ b/.changeset/fast-rules-complain.md @@ -0,0 +1,13 @@ +--- +"@wso2is/admin.applications.v1": minor +"@wso2is/console": minor +"@wso2is/admin.application-templates.v1": major +"@wso2is/admin.template-core.v1": major +"@wso2is/admin.connections.v1": patch +"@wso2is/admin.extensions.v1": patch +"@wso2is/admin.core.v1": patch +"@wso2is/unit-testing": patch +"@wso2is/i18n": patch +--- + +Update the application section to support SSO integration templates diff --git a/.changeset/giant-crabs-worry.md b/.changeset/giant-crabs-worry.md new file mode 100644 index 00000000000..1f9b86ef065 --- /dev/null +++ b/.changeset/giant-crabs-worry.md @@ -0,0 +1,7 @@ +--- +"@wso2is/react-components": minor +--- + +- Improve the React Markdown component to have the documentation theme +- Add support for automatic tab redirection based on tab ID +- Enhance the link component to include a title attribute diff --git a/.changeset/rich-buses-matter.md b/.changeset/rich-buses-matter.md new file mode 100644 index 00000000000..f5fb72ed7b3 --- /dev/null +++ b/.changeset/rich-buses-matter.md @@ -0,0 +1,5 @@ +--- +"@wso2is/form": minor +--- + +Add a file-picker adapter and update the style of the text field adapter for read-only mode diff --git a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 index aef08815dc1..77e2c4444ab 100644 --- a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 +++ b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 @@ -1683,6 +1683,13 @@ {% endfor %} {% endif %} ], + "hiddenApplicationTemplates": [ + {% if console.ui.hiddenApplicationTemplates is defined %} + {% for value in console.ui.hiddenApplicationTemplates %} + "{{ value }}"{{ "," if not loop.last }} + {% endfor %} + {% endif %} + ], "hiddenUserStores": [ {% if console.ui.hidden_user_stores is defined %} {% for value in console.ui.hidden_user_stores %} diff --git a/apps/console/package.json b/apps/console/package.json index ac0166d86a4..635de1109c5 100644 --- a/apps/console/package.json +++ b/apps/console/package.json @@ -132,7 +132,9 @@ "slashes": "^2.0.2", "styled-components": "^4.4.1", "swr": "^2.0.0", - "uuid": "^8.3.0" + "uuid": "^8.3.0", + "@wso2is/admin.application-templates.v1": "^1.0.0", + "@wso2is/admin.template-core.v1": "^1.0.0" }, "devDependencies": { "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", diff --git a/apps/console/src/app.tsx b/apps/console/src/app.tsx index df3c8f26252..238d3661db7 100755 --- a/apps/console/src/app.tsx +++ b/apps/console/src/app.tsx @@ -18,6 +18,7 @@ import { BasicUserInfo, DecodedIDTokenPayload, useAuthContext } from "@asgardeo/auth-react"; import { AccessControlProvider, AllFeatureInterface, FeatureGateInterface } from "@wso2is/access-control"; +import { ApplicationTemplateConstants } from "@wso2is/admin.application-templates.v1/constants/templates"; import { EventPublisher, PreLoader } from "@wso2is/admin.core.v1"; import { ProtectedRoute } from "@wso2is/admin.core.v1/components"; import { Config, DocumentationLinks } from "@wso2is/admin.core.v1/configs"; @@ -34,6 +35,8 @@ import { AppState } from "@wso2is/admin.core.v1/store"; import { commonConfig } from "@wso2is/admin.extensions.v1"; import { useGetAllFeatures } from "@wso2is/admin.extensions.v1/components/feature-gate/api/feature-gate"; import { featureGateConfig } from "@wso2is/admin.extensions.v1/configs/feature-gate"; +import { ResourceTypes } from "@wso2is/admin.template-core.v1/models/templates"; +import ExtensionTemplatesProvider from "@wso2is/admin.template-core.v1/provider/extension-templates-provider"; import { AppConstants as CommonAppConstants } from "@wso2is/core/constants"; import { IdentityAppsApiException } from "@wso2is/core/exceptions"; import { CommonHelpers, isPortalAccessGranted } from "@wso2is/core/helpers"; @@ -466,46 +469,51 @@ export const App: FunctionComponent> = (): ReactElement => ) } /> - - - { - baseRoutes.map((route: RouteInterface, index: number) => { - return ( - route.protected ? - ( - - ) - : - ( - ) => { - return (); + + + + { + baseRoutes.map((route: RouteInterface, index: number) => { + return ( + route.protected ? + ( + + ) + : + ( + ) => { + return (); + } } - } - key={ index } - exact={ route.exact } - /> - ) - ); - }) - } - + key={ index } + exact={ route.exact } + /> + ) + ); + }) + } + + diff --git a/apps/console/src/configs/routes.tsx b/apps/console/src/configs/routes.tsx index 7c456bcd963..e075018f276 100644 --- a/apps/console/src/configs/routes.tsx +++ b/apps/console/src/configs/routes.tsx @@ -301,7 +301,7 @@ export const getAppViewRoutes = (): RouteInterface[] => { category: "console:develop.features.sidePanel.categories.application", children: [ { - component: lazy(() => import("@wso2is/admin.applications.v1/pages/application-template")), + component: lazy(() => import("@wso2is/admin.application-templates.v1/pages/application-template")), exact: true, icon: { icon: getSidePanelIcons().childIcon diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json index 72897c91168..84b32f0a8ff 100644 --- a/apps/console/src/public/deployment.config.json +++ b/apps/console/src/public/deployment.config.json @@ -1100,6 +1100,7 @@ "gravatarConfig": { "fallback": "404" }, + "hiddenApplicationTemplates": [], "hiddenAuthenticators": [ "BasicAuthRequestPathAuthenticator", "OAuthRequestPathAuthenticator" diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/custom-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/custom-template.svg new file mode 100644 index 00000000000..2e6f5bf8a52 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/custom-template.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/m2m-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/m2m-template.svg new file mode 100644 index 00000000000..1668e084cbf --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/m2m-template.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/mobile-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/mobile-template.svg new file mode 100644 index 00000000000..24c84f76b78 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/mobile-template.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/salesforce.png b/apps/console/src/public/resources/applications/assets/images/illustrations/salesforce.png new file mode 100755 index 00000000000..18855a7c3b6 Binary files /dev/null and b/apps/console/src/public/resources/applications/assets/images/illustrations/salesforce.png differ diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/spa-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/spa-template.svg new file mode 100644 index 00000000000..0d55899776b --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/spa-template.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/standard-based-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/standard-based-template.svg new file mode 100644 index 00000000000..1faa65a8338 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/standard-based-template.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/illustrations/traditional-template.svg b/apps/console/src/public/resources/applications/assets/images/illustrations/traditional-template.svg new file mode 100644 index 00000000000..c3509d19fbe --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/illustrations/traditional-template.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/android-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/android-logo.svg new file mode 100644 index 00000000000..a87c8a9b9bf --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/android-logo.svg @@ -0,0 +1,29 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/angular-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/angular-logo.svg new file mode 100644 index 00000000000..3fae37b64a2 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/angular-logo.svg @@ -0,0 +1,28 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/dotnet-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/dotnet-logo.svg new file mode 100644 index 00000000000..0f2870fc828 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/dotnet-logo.svg @@ -0,0 +1,31 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/flutter-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/flutter-logo.svg new file mode 100644 index 00000000000..71a965c1b3e --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/flutter-logo.svg @@ -0,0 +1,36 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/ios-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/ios-logo.svg new file mode 100644 index 00000000000..e8fbd0b9813 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/ios-logo.svg @@ -0,0 +1,21 @@ + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/java-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/java-logo.svg new file mode 100644 index 00000000000..b7ab6b68d24 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/java-logo.svg @@ -0,0 +1,32 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/javascript-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/javascript-logo.svg new file mode 100644 index 00000000000..f0847010b03 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/javascript-logo.svg @@ -0,0 +1,24 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/nodejs-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/nodejs-logo.svg new file mode 100644 index 00000000000..484c2fa89fd --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/nodejs-logo.svg @@ -0,0 +1,27 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/oauth2.png b/apps/console/src/public/resources/applications/assets/images/technologies/oauth2.png new file mode 100644 index 00000000000..86b72e63a1b Binary files /dev/null and b/apps/console/src/public/resources/applications/assets/images/technologies/oauth2.png differ diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/openid-connect.png b/apps/console/src/public/resources/applications/assets/images/technologies/openid-connect.png new file mode 100644 index 00000000000..154fb9bbba2 Binary files /dev/null and b/apps/console/src/public/resources/applications/assets/images/technologies/openid-connect.png differ diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/php-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/php-logo.svg new file mode 100644 index 00000000000..7285cb3620e --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/php-logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/react-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/react-logo.svg new file mode 100644 index 00000000000..bf4666a5c30 --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/react-logo.svg @@ -0,0 +1,27 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/saml.png b/apps/console/src/public/resources/applications/assets/images/technologies/saml.png new file mode 100644 index 00000000000..f61e5124c2d Binary files /dev/null and b/apps/console/src/public/resources/applications/assets/images/technologies/saml.png differ diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/vue-logo.svg b/apps/console/src/public/resources/applications/assets/images/technologies/vue-logo.svg new file mode 100644 index 00000000000..d8273fafc6a --- /dev/null +++ b/apps/console/src/public/resources/applications/assets/images/technologies/vue-logo.svg @@ -0,0 +1,30 @@ + + + + + diff --git a/apps/console/src/public/resources/applications/assets/images/technologies/ws-fed.png b/apps/console/src/public/resources/applications/assets/images/technologies/ws-fed.png new file mode 100644 index 00000000000..45de684962f Binary files /dev/null and b/apps/console/src/public/resources/applications/assets/images/technologies/ws-fed.png differ diff --git a/features/admin.application-templates.v1/api/use-get-application-template-metadata.ts b/features/admin.application-templates.v1/api/use-get-application-template-metadata.ts new file mode 100644 index 00000000000..b69941efc35 --- /dev/null +++ b/features/admin.application-templates.v1/api/use-get-application-template-metadata.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import useRequest, { + RequestConfigInterface, + RequestErrorInterface, + RequestResultInterface +} from "@wso2is/admin.core.v1/hooks/use-request"; +import { store } from "@wso2is/admin.core.v1/store"; +import { HttpMethods } from "@wso2is/core/models"; +import { ApplicationTemplateMetadataInterface } from "../models/templates"; + +/** + * Hook to fetches the application template metadata from the API. + * + * @param id - The id of the application template. + * @returns A promise containing the response. + */ +const useGetApplicationTemplateMetadata = ( + id: string, + shouldFetch: boolean = true +): RequestResultInterface => { + + const requestConfig: RequestConfigInterface = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + url: store?.getState()?.config?.endpoints?.applicationTemplateMetadata?.replace("{{id}}", id) + }; + + const { data, error, isValidating, mutate } = useRequest(shouldFetch ? requestConfig : null); + + return { + data, + error, + isLoading: shouldFetch && !error && !data, + isValidating, + mutate + }; +}; + +export default useGetApplicationTemplateMetadata; diff --git a/features/admin.application-templates.v1/api/use-get-application-template.ts b/features/admin.application-templates.v1/api/use-get-application-template.ts new file mode 100644 index 00000000000..78b7ba4d889 --- /dev/null +++ b/features/admin.application-templates.v1/api/use-get-application-template.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import useRequest, { + RequestConfigInterface, + RequestErrorInterface, + RequestResultInterface +} from "@wso2is/admin.core.v1/hooks/use-request"; +import { store } from "@wso2is/admin.core.v1/store"; +import { HttpMethods } from "@wso2is/core/models"; +import { ApplicationTemplateInterface } from "../models/templates"; + +/** + * Hook to fetch the application template details from the API. + * + * @param id - The id of the application template. + * @returns A promise containing the response. + */ +const useGetApplicationTemplate = ( + id: string, + shouldFetch: boolean = true +): RequestResultInterface => { + const requestConfig: RequestConfigInterface = { + headers: { + Accept: "application/json", + "Content-Type": "application/json" + }, + method: HttpMethods.GET, + url: store?.getState()?.config?.endpoints?.applicationTemplate?.replace("{{id}}", id) + }; + + const { data, error, isValidating, mutate } = useRequest(shouldFetch ? requestConfig : null); + + return { + data, + error, + isLoading: shouldFetch && !error && !data, + isValidating, + mutate + }; +}; + +export default useGetApplicationTemplate; diff --git a/features/admin.application-templates.v1/components/application-create-wizard.tsx b/features/admin.application-templates.v1/components/application-create-wizard.tsx new file mode 100644 index 00000000000..41c3ce148ac --- /dev/null +++ b/features/admin.application-templates.v1/components/application-create-wizard.tsx @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createApplication } from "@wso2is/admin.applications.v1/api"; +import { ApplicationShareModal } from "@wso2is/admin.applications.v1/components/modals/application-share-modal"; +import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; +import useApplicationSharingEligibility from "@wso2is/admin.applications.v1/hooks/use-application-sharing-eligibility"; +import { MainApplicationInterface, URLFragmentTypes } from "@wso2is/admin.applications.v1/models"; +import { AppState, TierLimitReachErrorModal } from "@wso2is/admin.core.v1"; +import { AppConstants } from "@wso2is/admin.core.v1/constants/app-constants"; +import { history } from "@wso2is/admin.core.v1/helpers/history"; +import { EventPublisher } from "@wso2is/admin.core.v1/utils/event-publisher"; +import { ResourceCreateWizard } from "@wso2is/admin.template-core.v1/components/resource-create-wizard"; +import { DynamicFieldInterface, DynamicFormInterface } from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { AxiosError, AxiosResponse } from "axios"; +import cloneDeep from "lodash-es/cloneDeep"; +import get from "lodash-es/get"; +import unset from "lodash-es/unset"; +import React, { FunctionComponent, ReactElement, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { Dispatch } from "redux"; +import { ModalProps } from "semantic-ui-react"; +import { ApplicationTemplateConstants } from "../constants/templates"; +import useApplicationTemplate from "../hooks/use-application-template"; +import useApplicationTemplateMetadata from "../hooks/use-application-template-metadata"; +import useInitializeHandlers from "../hooks/use-custom-initialize-handlers"; +import useSubmissionHandlers from "../hooks/use-custom-submission-handlers"; +import useValidationHandlers from "../hooks/use-custom-validation-handlers"; + +/** + * Prop types of the `ApplicationCreateWizard` component. + */ +export interface ApplicationCreateWizardPropsInterface extends ModalProps, IdentifiableComponentInterface { + /** + * Callback triggered when closing the application creation wizard. + */ + onClose: () => void; +} + +/** + * Dynamic application create wizard component. + * + * @param Props - Props to be injected into the component. + */ +export const ApplicationCreateWizard: FunctionComponent = ({ + ["data-componentid"]: componentId = "application-create-wizard", + onClose +}: ApplicationCreateWizardPropsInterface): ReactElement => { + + const { customValidations } = useValidationHandlers(); + const { customInitializers } = useInitializeHandlers(); + const { customSubmissionHandlers } = useSubmissionHandlers(); + const { + template: templateData, + isTemplateRequestLoading: isTemplateDataFetchRequestLoading + } = useApplicationTemplate(); + const { + templateMetadata, + isTemplateMetadataRequestLoading: isTemplateMetadataFetchRequestLoading + } = useApplicationTemplateMetadata(); + const isApplicationSharable: boolean = useApplicationSharingEligibility(); + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + const [ showApplicationShareModal, setShowApplicationShareModal ] = useState(false); + const [ lastCreatedApplicationId, setLastCreatedApplicationId ] = useState(null); + const [ openLimitReachedModal, setOpenLimitReachedModal ] = useState(false); + + const isClientSecretHashEnabled: boolean = useSelector((state: AppState) => + state?.config?.ui?.isClientSecretHashEnabled); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + /** + * Apply additional conditions to filter the form fields. + */ + const formDefinition: DynamicFormInterface = useMemo(() => { + if (!templateMetadata) { + return null; + } + + const form: DynamicFormInterface = cloneDeep(templateMetadata?.create?.form); + + if (!isApplicationSharable) { + form.fields = form?.fields?.filter((field: DynamicFieldInterface) => + field?.name !== ApplicationTemplateConstants.APPLICATION_CREATE_WIZARD_SHARING_FIELD_NAME); + } + + return form; + }, [ templateMetadata, isApplicationSharable ]); + + /** + * Prepare initial values for the resource create wizard. + */ + const formInitialValues: Record = useMemo(() => { + if (templateData?.payload) { + const clonedPayload: MainApplicationInterface = cloneDeep(templateData?.payload); + + if (!clonedPayload?.templateId) { + clonedPayload.templateId = templateData?.id; + } + if (!clonedPayload?.templateVersion && templateData?.version) { + clonedPayload.templateVersion = templateData?.version; + } + + return clonedPayload as unknown as Record; + } + + return null; + }, [ templateData ]); + + /** + * After the application is created, the user will be redirected to the + * edit page using this function. + * + * @param createdAppId - ID of the created application. + */ + const handleAppCreationComplete = (createdAppId: string): void => { + // The created resource's id is sent as a location header. + // If that's available, navigate to the edit page. + if (createdAppId) { + let searchParams: string = "?"; + const defaultTabIndex: number | string = templateMetadata?.edit?.defaultActiveTabId ?? 0; + + if (isClientSecretHashEnabled) { + searchParams = `${ searchParams }${ + ApplicationManagementConstants.CLIENT_SECRET_HASH_ENABLED_URL_SEARCH_PARAM_KEY }=true`; + } + + history.push({ + hash: `#${URLFragmentTypes.TAB_INDEX}${defaultTabIndex}`, + pathname: AppConstants.getPaths()?.get("APPLICATION_EDIT")?.replace(":id", createdAppId), + search: searchParams + }); + + return; + } + + // Fallback to applications page, if the location header is not present. + history.push(AppConstants.getPaths().get("APPLICATIONS")); + }; + + /** + * Function to handle wizard form submission. + * + * @param values - Submission values from the form fields. + * @param callback - Callback function to execute after form submission is complete. + */ + const handleFormSubmission = ( + values: Record, + callback: (errorMsg: string, errorDescription: string) => void + ): void => { + const isApplicationSharingEnabled: boolean = get( + values, + ApplicationTemplateConstants.APPLICATION_CREATE_WIZARD_SHARING_FIELD_NAME + ) as boolean ?? false; + + unset(values, ApplicationTemplateConstants.APPLICATION_CREATE_WIZARD_SHARING_FIELD_NAME); + + createApplication(values as unknown as MainApplicationInterface) + .then((response: AxiosResponse) => { + eventPublisher.compute(() => { + eventPublisher.publish("application-register-new-application", { + type: templateData?.id + }); + }); + + dispatch(addAlert({ + description: t("applications:notifications.addApplication.success.description"), + level: AlertLevels.SUCCESS, + message: t("applications:notifications.addApplication.success.message") + })); + + const location: string = response.headers.location; + const createdAppID: string = location.substring(location.lastIndexOf("/") + 1); + + callback(null, null); + + if (isApplicationSharingEnabled) { + setLastCreatedApplicationId(createdAppID); + setShowApplicationShareModal(true); + } else { + handleAppCreationComplete(createdAppID); + } + }) + .catch((error: AxiosError) => { + if (error?.response?.status === 403 + && error?.response?.data?.code === ApplicationManagementConstants + .ERROR_CREATE_LIMIT_REACHED.getErrorCode()) { + setOpenLimitReachedModal(true); + + return; + } + + if (error?.response?.data?.description) { + callback(error.response.data.description, t( + "applications:notifications.addApplication.error.message" + )); + + return; + } + + callback( + t("applications:notifications.addApplication.genericError.description"), + t("applications:notifications.addApplication.error.message") + ); + }); + }; + + if (openLimitReachedModal) { + return ( + { + setOpenLimitReachedModal(false); + onClose(); + } + } + header={ t( + "applications:notifications.tierLimitReachedError.heading" + ) } + description={ t( + "applications:notifications." + + "tierLimitReachedError.emptyPlaceholder.subtitles" + ) } + message={ t( + "applications:notifications." + + "tierLimitReachedError.emptyPlaceholder.title" + ) } + openModal={ openLimitReachedModal } + /> + ); + } + + if (showApplicationShareModal) { + return ( + setShowApplicationShareModal(false) } + onApplicationSharingCompleted={ () => { + setShowApplicationShareModal(false); + handleAppCreationComplete(lastCreatedApplicationId); + setLastCreatedApplicationId(null); + } } + /> + ); + } + + return ( + } + buttonText={ t("common:create") } + onFormSubmit={ handleFormSubmission } + isLoading={ isTemplateDataFetchRequestLoading || isTemplateMetadataFetchRequestLoading } + data-componentid={ componentId } + /> + ); +}; diff --git a/features/admin.application-templates.v1/components/application-creation-adapter.tsx b/features/admin.application-templates.v1/components/application-creation-adapter.tsx new file mode 100644 index 00000000000..b21a20cbaa1 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-creation-adapter.tsx @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MinimalAppCreateWizard } from + "@wso2is/admin.applications.v1/components/wizard/minimal-application-create-wizard"; +import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; +import { ApplicationTemplateListItemInterface } from "@wso2is/admin.applications.v1/models"; +import { ApplicationTemplateManagementUtils } from + "@wso2is/admin.applications.v1/utils/application-template-management-utils"; +import { AppState } from "@wso2is/admin.core.v1"; +import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import { ContentLoader } from "@wso2is/react-components"; +import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { ApplicationCreateWizard } from "./application-create-wizard"; +import { ApplicationTemplateCategories } from "../models/templates"; + +/** + * Props for the Application creation adapter component. + */ +export interface ApplicationCreationAdapterPropsInterface extends IdentifiableComponentInterface { + /** + * Template for rendering the application creation wizard. + */ + template: ExtensionTemplateListInterface; + /** + * Indicator of whether the application creation wizard should be displayed or not. + */ + showWizard: boolean; + /** + * Callback triggered when closing the application creation wizard. + */ + onClose: () => void; +} + +/** + * Adapter for rendering the application creation wizard. + * + * @param props - Props injected to the component. + * + * @returns Application creation adapter component. + */ +const ApplicationCreationAdapter: FunctionComponent = ({ + template, + showWizard, + onClose, + "data-componentid": _componentId = "application-creation-adapter" +}: ApplicationCreationAdapterPropsInterface): ReactElement => { + + const oldApplicationTemplates: ApplicationTemplateListItemInterface[] = useSelector( + (state: AppState) => state?.application?.groupedTemplates); + + const [ + isOldApplicationTemplateRequestLoading, + setOldApplicationTemplateRequestLoadingStatus + ] = useState(false); + + /** + * Get old Application templates. + */ + useEffect(() => { + if (oldApplicationTemplates !== undefined) { + return; + } + + setOldApplicationTemplateRequestLoadingStatus(true); + + ApplicationTemplateManagementUtils.getApplicationTemplates() + .finally(() => { + setOldApplicationTemplateRequestLoadingStatus(false); + }); + }, [ oldApplicationTemplates ]); + + /** + * Render the appropriate Application Creation Wizard based on the template category. + * + * @returns - Application Create Wizard Component. + */ + const renderApplicationCreationWizard = (): ReactElement => { + if (!template) { + return null; + } + + const oldApplicationTemplate: ApplicationTemplateListItemInterface = oldApplicationTemplates?.find( + (oldTemplate: ApplicationTemplateListItemInterface) => oldTemplate?.templateId === template?.id); + + switch(template?.category) { + case ApplicationTemplateCategories.DEFAULT: + return ( + onClose() } + template={ oldApplicationTemplate } + showHelpPanel={ true } + subTemplates={ oldApplicationTemplate?.subTemplates } + subTemplatesSectionTitle={ oldApplicationTemplate?.subTemplatesSectionTitle } + addProtocol={ false } + templateLoadingStrategy={ ApplicationManagementConstants.DEFAULT_APP_TEMPLATE_LOADING_STRATEGY } + /> + ); + default: + return ( + + ); + } + }; + + return ( + showWizard + ? isOldApplicationTemplateRequestLoading + ? + : renderApplicationCreationWizard() + : null + ); +}; + +export default ApplicationCreationAdapter; diff --git a/features/admin.application-templates.v1/components/application-edit-form.tsx b/features/admin.application-templates.v1/components/application-edit-form.tsx new file mode 100644 index 00000000000..b91302ab1c8 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-edit-form.tsx @@ -0,0 +1,293 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { updateApplicationDetails, updateAuthProtocolConfig } from "@wso2is/admin.applications.v1/api"; +import { + ApplicationInterface, + MainApplicationInterface, + OIDCDataInterface, + PassiveStsConfigurationInterface, + SAML2ConfigurationInterface, + SAML2ServiceProviderInterface, + SupportedAuthProtocolTypes, + WSTrustConfigurationInterface +} from "@wso2is/admin.applications.v1/models"; +import { TemplateDynamicForm } from "@wso2is/admin.template-core.v1/components/template-dynamic-form"; +import { DynamicFieldInterface } from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { AxiosError } from "axios"; +import cloneDeep from "lodash-es/cloneDeep"; +import isEqual from "lodash-es/isEqual"; +import pick from "lodash-es/pick"; +import unset from "lodash-es/unset"; +import React, { FunctionComponent, ReactElement, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import { AuthProtocolTypes } from "../../admin.connections.v1"; +import { ApplicationTemplateConstants } from "../constants/templates"; +import useApplicationTemplate from "../hooks/use-application-template"; +import useInitializeHandlers from "../hooks/use-custom-initialize-handlers"; +import useSubmissionHandlers from "../hooks/use-custom-submission-handlers"; +import useValidationHandlers from "../hooks/use-custom-validation-handlers"; +import { ApplicationEditTabMetadataInterface } from "../models/templates"; + +/** + * Prop types of the `ApplicationEditForm` component. + */ +export interface ApplicationEditFormPropsInterface extends IdentifiableComponentInterface { + /** + * The tab metadata to be used for the application edit form generation. + */ + tab: ApplicationEditTabMetadataInterface; + /** + * Current editing application data. + */ + application: ApplicationInterface; + /** + * Current application protocol name. + */ + protocolName: string; + /** + * Current editing application inbound protocol data. + */ + inboundProtocolConfigurations: OIDCDataInterface | SAML2ConfigurationInterface | WSTrustConfigurationInterface + | PassiveStsConfigurationInterface; + /** + * Is the application info request loading. + */ + isLoading?: boolean; + /** + * Callback to update the application details. + */ + onUpdate: (id: string) => void; + /** + * Callback to update the application protocol details. + */ + onProtocolUpdate: () => void; + /** + * Make the form read only. + */ + readOnly?: boolean; +} + +/** + * Dynamic application edit form component. + * + * @param Props - Props to be injected into the component. + */ +export const ApplicationEditForm: FunctionComponent = ({ + tab, + application, + protocolName, + inboundProtocolConfigurations, + isLoading, + onUpdate, + onProtocolUpdate, + readOnly, + ["data-componentid"]: componentId = "application-edit-form" +}: ApplicationEditFormPropsInterface): ReactElement => { + + const { customValidations } = useValidationHandlers(); + const { customInitializers } = useInitializeHandlers(); + const { customSubmissionHandlers } = useSubmissionHandlers(); + const { + template: templateData, + isTemplateRequestLoading: isTemplateDataFetchRequestLoading + } = useApplicationTemplate(); + + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + + /** + * Prepare the initial value object for the application edit form. + */ + const initialValues: MainApplicationInterface = useMemo(() => { + if (!inboundProtocolConfigurations || !application || !protocolName) { + return null; + } + + const formInitialValues: MainApplicationInterface = cloneDeep(application); + let protocolKeyName: string = protocolName; + + if (SupportedAuthProtocolTypes.WS_FEDERATION === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_FEDERATION ]; + } else if (SupportedAuthProtocolTypes.WS_TRUST === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_TRUST ]; + } + + if (SupportedAuthProtocolTypes.SAML === protocolKeyName) { + formInitialValues.inboundProtocolConfiguration = { + [ protocolKeyName ]: { + manualConfiguration: inboundProtocolConfigurations as SAML2ServiceProviderInterface + } + }; + } else { + formInitialValues.inboundProtocolConfiguration = { + [ protocolKeyName ]: inboundProtocolConfigurations + }; + } + + return formInitialValues; + }, [ inboundProtocolConfigurations, application ]); + + /** + * Function to handle form submission. + * + * @param values - Submission values from the form fields. + * @param callback - Callback function to execute after form submission is complete. + */ + const handleFormSubmission = (values: Record, callback: () => void): void => { + let protocolKeyName: string = protocolName; + + if (SupportedAuthProtocolTypes.WS_FEDERATION === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_FEDERATION ]; + } else if (SupportedAuthProtocolTypes.WS_TRUST === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_TRUST ]; + } + + const editPaths: string[] = tab?.form?.fields?.map((field: DynamicFieldInterface) => field?.name); + let protocolConfigurations: Record; + let applicationConfigurations: Record; + + if (values?.inboundProtocolConfiguration?.[protocolKeyName]) { + if (SupportedAuthProtocolTypes.SAML === protocolKeyName) { + if (values?.inboundProtocolConfiguration?.[protocolKeyName]?.manualConfiguration) { + if (!isEqual(values?.inboundProtocolConfiguration?.[protocolKeyName]?.manualConfiguration, + inboundProtocolConfigurations)) { + protocolConfigurations = values?.inboundProtocolConfiguration?.[protocolKeyName]; + } + } else { + protocolConfigurations = values?.inboundProtocolConfiguration?.[protocolKeyName]; + } + } else { + if (!isEqual(values?.inboundProtocolConfiguration?.[protocolKeyName], inboundProtocolConfigurations)) { + protocolConfigurations = values?.inboundProtocolConfiguration?.[protocolKeyName]; + } + } + unset(values, "inboundProtocolConfiguration"); + } + + values.id = application?.id; + if (!isEqual(values, application)) { + editPaths.push("id"); + applicationConfigurations = pick(values, editPaths); + } + + const updateProtocolConfigurations = () => { + updateAuthProtocolConfig( + application?.id, + protocolConfigurations, + protocolName + ).then(() => { + onProtocolUpdate(); + + dispatch(addAlert({ + description: t("applications:notifications.updateApplication.success" + + ".description"), + level: AlertLevels.SUCCESS, + message: t("applications:notifications.updateApplication.success.message") + })); + }).catch((error: AxiosError) => { + if (error?.response?.data?.description) { + dispatch(addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications.updateInboundProtocolConfig" + + ".error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applications:notifications.updateInboundProtocolConfig" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications.updateInboundProtocolConfig" + + ".genericError.message") + })); + }).finally(() => callback()); + }; + + if (applicationConfigurations && Object.keys(applicationConfigurations)?.length > 0) { + updateApplicationDetails(applicationConfigurations as unknown as ApplicationInterface) + .then(() => { + onUpdate(application?.id); + + if (protocolConfigurations) { + updateProtocolConfigurations(); + } else { + callback(); + + dispatch(addAlert({ + description: t("applications:notifications.updateApplication.success" + + ".description"), + level: AlertLevels.SUCCESS, + message: t("applications:notifications.updateApplication.success.message") + })); + } + }) + .catch((error: AxiosError) => { + callback(); + + if (error?.response?.data?.description) { + dispatch(addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications.updateApplication.error" + + ".message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applications:notifications.updateApplication" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications.updateApplication.genericError" + + ".message") + })); + }); + } else if (protocolConfigurations) { + updateProtocolConfigurations(); + } + }; + + return ( + } + templatePayload={ templateData?.payload as unknown as Record } + buttonText={ t("common:update") } + onFormSubmit={ handleFormSubmission } + isLoading={ isLoading || isTemplateDataFetchRequestLoading } + readOnly={ readOnly } + data-componentid={ componentId } + /> + ); +}; diff --git a/features/admin.application-templates.v1/components/application-markdown-guide.tsx b/features/admin.application-templates.v1/components/application-markdown-guide.tsx new file mode 100644 index 00000000000..c0c17194edf --- /dev/null +++ b/features/admin.application-templates.v1/components/application-markdown-guide.tsx @@ -0,0 +1,196 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { + ApplicationInterface, + OIDCApplicationConfigurationInterface, + OIDCDataInterface, + PassiveStsConfigurationInterface, + SAML2ConfigurationInterface, + SAMLApplicationConfigurationInterface, + SupportedAuthProtocolTypes, + WSTrustConfigurationInterface +} from "@wso2is/admin.applications.v1/models"; +import { AppState } from "@wso2is/admin.core.v1"; +import { MarkdownGuide } from "@wso2is/admin.template-core.v1/components/markdown-guide"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import set from "lodash-es/set"; +import React, { FunctionComponent, ReactElement, useMemo } from "react"; +import { useSelector } from "react-redux"; +import { AuthProtocolTypes } from "../../admin.connections.v1"; +import { ApplicationTemplateConstants } from "../constants/templates"; + +/** + * Prop types of the `ApplicationMarkdownGuide` component. + */ +export interface ApplicationMarkdownGuidePropsInterface extends IdentifiableComponentInterface { + /** + * Current editing application data. + */ + application: ApplicationInterface; + /** + * Current editing application inbound protocol data. + */ + inboundProtocolConfigurations: OIDCDataInterface | SAML2ConfigurationInterface | WSTrustConfigurationInterface + | PassiveStsConfigurationInterface; + /** + * Content to be displayed in Markdown format. + */ + content: string; + /** + * Is the application info request loading. + */ + isLoading?: boolean; + /** + * Current application protocol name. + */ + protocolName: string; +} + +/** + * An interface that includes all the moderated data types using initial data. + */ +interface ModeratedData { + pemCertificate?: string; +} + +/** + * An interface that includes all the data types which can be used in the markdown guide. + */ +interface MarkdownGuideDataInterface { + general?: ApplicationInterface; + protocol?: { + oidc?: OIDCDataInterface; + saml?: SAML2ConfigurationInterface; + wsTrust?: WSTrustConfigurationInterface; + passiveSts?: PassiveStsConfigurationInterface; + }; + metadata?: { + saml?: SAMLApplicationConfigurationInterface; + odic?: OIDCApplicationConfigurationInterface; + }; + tenantDomain?: string; + clientOrigin?: string; + serverOrigin?: string; + moderatedData?: ModeratedData; +} + +/** + * Application markdown guide generation component. + * + * @param Props - Props to be injected into the component. + */ +export const ApplicationMarkdownGuide: FunctionComponent = ({ + application, + inboundProtocolConfigurations, + content, + isLoading, + protocolName, + ["data-componentid"]: componentId = "application-markdown-guide" +}: ApplicationMarkdownGuidePropsInterface): ReactElement => { + + const samlConfigurations: SAMLApplicationConfigurationInterface = useSelector( + (state: AppState) => state?.application?.samlConfigurations); + const oidcConfigurations: SAMLApplicationConfigurationInterface = useSelector( + (state: AppState) => state?.application?.oidcConfigurations); + const tenantDomain: string = useSelector((state: AppState) => state?.auth?.tenantDomain); + const clientOrigin: string = useSelector((state: AppState) => state?.config?.deployment?.clientOrigin); + const serverOrigin: string = useSelector((state: AppState) => state?.config?.deployment?.idpConfigs?.serverOrigin); + + /** + * Convert certificate into the pem format. + * + * @param cert - Certificate in string format. + * @returns Pem format certificate content. + */ + function getPemFormatCertificate(cert: string): string { + const header: string = "-----BEGIN CERTIFICATE-----"; + const footer: string = "-----END CERTIFICATE-----"; + const lineLength: number = 64; + + // Insert line breaks every `lineLength` characters. + const formattedCert: string = cert.match(new RegExp(".{1," + lineLength + "}", "g"))?.join("\n") || ""; + + return `${header}\n${formattedCert}\n${footer}`; + } + + /** + * Prepare moderated data using the initial API response data. + */ + const getModeratedData = (): ModeratedData => { + const data: ModeratedData = {}; + + if (samlConfigurations?.certificate) { + data.pemCertificate = btoa(getPemFormatCertificate(samlConfigurations?.certificate)); + } + + return data; + }; + + /** + * Create a unified data object for the current application + * by combining multiple API responses. + */ + const data: MarkdownGuideDataInterface = useMemo(() => { + if (!application || !inboundProtocolConfigurations || !samlConfigurations || !oidcConfigurations + || !tenantDomain || !clientOrigin || !serverOrigin + ) { + return null; + } + + let protocolKeyName: string = protocolName; + + if (SupportedAuthProtocolTypes.WS_FEDERATION === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_FEDERATION ]; + } else if (SupportedAuthProtocolTypes.WS_TRUST === protocolKeyName) { + protocolKeyName = ApplicationTemplateConstants.APPLICATION_INBOUND_PROTOCOL_KEYS[ + AuthProtocolTypes.WS_TRUST ]; + } + + const markdownDataObject: MarkdownGuideDataInterface = {}; + + markdownDataObject.general = application; + set(markdownDataObject, `protocol.${protocolKeyName}`, inboundProtocolConfigurations); + set(markdownDataObject, "metadata.saml", samlConfigurations); + set(markdownDataObject, "metadata.oidc", samlConfigurations); + markdownDataObject.tenantDomain = tenantDomain; + markdownDataObject.clientOrigin = clientOrigin; + markdownDataObject.serverOrigin = serverOrigin; + markdownDataObject.moderatedData = getModeratedData(); + + return markdownDataObject; + }, [ + application, + inboundProtocolConfigurations, + samlConfigurations, + oidcConfigurations, + tenantDomain, + clientOrigin, + serverOrigin + ]); + + return ( + } + content={ content } + isLoading={ isLoading } + data-componentid={ componentId } + /> + ); +}; diff --git a/features/admin.application-templates.v1/components/application-tab-components-filter.tsx b/features/admin.application-templates.v1/components/application-tab-components-filter.tsx new file mode 100644 index 00000000000..c7addbc0423 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-tab-components-filter.tsx @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import { ContentLoader } from "@wso2is/react-components"; +import React, { FunctionComponent, PropsWithChildren, ReactElement, ReactNode, useMemo } from "react"; +import { Grid } from "semantic-ui-react"; +import { ApplicationTabIDs } from "../../admin.extensions.v1"; +import useApplicationTemplateMetadata from "../hooks/use-application-template-metadata"; +import { ApplicationEditTabMetadataInterface } from "../models/templates"; + +/** + * Prop types of the `ApplicationTabComponentsFilter` component. + */ +export interface ApplicationTabComponentsFilterPropsInterface extends IdentifiableComponentInterface { + /** + * Current tab id. + */ + tabId: ApplicationTabIDs; +} + +/** + * Application tab components filtering component. + * + * @param Props - Props to be injected into the component. + */ +export const ApplicationTabComponentsFilter: FunctionComponent< + PropsWithChildren +> = ({ + tabId, + children, + "data-componentid": _componentId = "application-tab-components-filter" +}: PropsWithChildren): ReactElement => { + + const { + templateMetadata, + isTemplateMetadataRequestLoading + } = useApplicationTemplateMetadata(); + + /** + * Extract the data-component IDs that need to be hidden in the current application tab. + */ + const hiddenComponents: string[] = useMemo(() => { + let componentList: string[] = []; + + templateMetadata?.edit?.tabs?.forEach((tab: ApplicationEditTabMetadataInterface) => { + if (tab?.id === tabId) { + if (tab?.hiddenComponents + && Array.isArray(tab?.hiddenComponents) + && tab?.hiddenComponents?.length > 0) { + componentList = tab?.hiddenComponents; + } + } + }); + + return componentList; + }, [ templateMetadata, tabId ]); + + const renderTabContents = (): ReactElement => { + if (isTemplateMetadataRequestLoading || !hiddenComponents) { + return ( + + + + + + ); + } else if (hiddenComponents?.length === 0) { + return ( + <> + { children } + + ); + } else { + return ( + <> + { + React.Children.map(children, (child: ReactNode) => { + const componentId: string = child?.["props"]?.["data-componentid"]; + + if (componentId && hiddenComponents?.includes(componentId)) { + return null; + } + + return child; + }) + } + + ); + } + }; + + return renderTabContents(); +}; diff --git a/features/admin.application-templates.v1/components/application-template-card.scss b/features/admin.application-templates.v1/components/application-template-card.scss new file mode 100644 index 00000000000..4b2744d2af3 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-template-card.scss @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.application-template { + display: flex; + flex-direction: column; + justify-content: flex-start; + width: 330px; + margin: 5px; + padding: 12px; + position: relative; + + &.disabled { + opacity: 0.6; + cursor: not-allowed !important; + filter: grayscale(100%); + + &:hover { + border-color: #d7d9e3 !important; + box-shadow: none; + } + } + + .application-template-ribbon { + position: absolute; + right: 0; + top: 13px; + font-size: .8em; + font-weight: 500; + color: #fff; + height: 24px; + width: 100px; + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: space-around; + clip-path: polygon(4% 0,0 100%,100% 100%,100% 0,100% 0); + -webkit-clip-path: polygon(4% 0,0 100%,100% 100%,100% 0,100% 0); + + &.coming-soon { + background: linear-gradient(270deg,#ff9d4d 0,#ff7300 100%); + } + + &.new { + background: #3fb81f; + } + } + + .application-template-header { + display: flex; + flex-direction: row; + gap: 15px; + align-content: center; + align-items: center; + padding-bottom: 0; + + .application-template-image-container { + height: 35px; + width: 35px; + justify-content: center; + display: flex; + flex-direction: row; + overflow: hidden; + + .application-template-image { + flex: 1; + height: 100%; + max-width: 100%; + object-fit: contain; + } + } + } + + .application-template-body { + padding-bottom: 16px; + + .application-template-description { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; /* Number of lines to show */ + /* Ensure compatibility with other browsers */ + height: calc(0.875rem * 1.5 * 2); /* Line height * number of lines */ + white-space: normal; + } + + .application-template-supported-technologies { + justify-content: flex-end; + margin-top: 10px; + + .application-template-supported-technology { + margin-left: 3px; + + &:last-child { + margin-left: -2px; + } + + .MuiAvatar-img { + object-fit: contain; + } + } + } + } +} + +.application-template:hover { + border-color: var(--oxygen-palette-primary-main); +} diff --git a/features/admin.application-templates.v1/components/application-template-card.tsx b/features/admin.application-templates.v1/components/application-template-card.tsx new file mode 100644 index 00000000000..c9b99fe8778 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-template-card.tsx @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import AvatarGroup from "@mui/material/AvatarGroup"; +import Avatar from "@oxygen-ui/react/Avatar"; +import Card from "@oxygen-ui/react/Card"; +import CardContent from "@oxygen-ui/react/CardContent"; +import Tooltip from "@oxygen-ui/react/Tooltip"; +import Typography from "@oxygen-ui/react/Typography"; +import { + CustomAttributeInterface, + ExtensionTemplateListInterface, + ResourceTypes +} from "@wso2is/admin.template-core.v1/models/templates"; +import { ExtensionTemplateManagementUtils } from "@wso2is/admin.template-core.v1/utils/templates"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import classnames from "classnames"; +import React, { FunctionComponent, MouseEvent, ReactElement, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { ApplicationTemplateConstants } from "../constants/templates"; +import "./application-template-card.scss"; +import { ApplicationTemplateFeatureStatus, SupportedTechnologyMetadataInterface } from "../models/templates"; + +/** + * Props for the application template card component. + */ +export interface ApplicationTemplateCardPropsInterface extends IdentifiableComponentInterface { + /** + * Callback function triggered upon clicking on the application template card. + * + * @param e - Click event. + */ + onClick?: (e: MouseEvent) => void; + /** + * Details needed to render the application template card. + */ + template: ExtensionTemplateListInterface; +} + +/** + * Application template card component. + * + * @param props - Props injected to the component. + * + * @returns Application template card component. + */ +const ApplicationTemplateCard: FunctionComponent = ({ + template, + onClick, + ["data-componentid"]: componentId = "application-template-card" +}: ApplicationTemplateCardPropsInterface): ReactElement => { + + const { t } = useTranslation(); + + /** + * Extract the supported technology details related to the current application template. + */ + const supportedTechnologies: SupportedTechnologyMetadataInterface[] = useMemo(() => { + if (!template?.customAttributes || + !Array.isArray(template?.customAttributes) || + template?.customAttributes?.length <= 0) { + + return []; + } + + const property: CustomAttributeInterface = template?.customAttributes?.find( + (property: CustomAttributeInterface) => + property?.key === ApplicationTemplateConstants.SUPPORTED_TECHNOLOGIES_ATTRIBUTE_KEY + ); + + return property?.value ? + typeof property?.value === "string" ? + JSON.parse(property?.value) : + property?.value : + []; + }, [ template ]); + + /** + * Resolve the current template feature status. + */ + const featureStatus: ApplicationTemplateFeatureStatus = useMemo(() => { + if (!template?.customAttributes + || !Array.isArray(template?.customAttributes) + || template?.customAttributes?.length <= 0) { + + return false; + } + + const property: CustomAttributeInterface = template?.customAttributes?.find( + (property: CustomAttributeInterface) => + property?.key === ApplicationTemplateConstants.FEATURE_STATUS_ATTRIBUTE_KEY + ); + + return property?.value; + }, [ template ]); + + /** + * Resolve the corresponding label for the current feature label. + * + * @param featureStatus - Feature status from the template. + * @returns The feature status label. + */ + const resolveRibbonLabel = (featureStatus: ApplicationTemplateFeatureStatus): string => { + switch (featureStatus) { + case ApplicationTemplateFeatureStatus.COMING_SOON: + return t("common:comingSoon"); + case ApplicationTemplateFeatureStatus.NEW: + return t("common:new"); + } + }; + + return ( + ) => { + if (featureStatus !== ApplicationTemplateFeatureStatus.COMING_SOON) { + onClick(e); + } + } + } + data-componentid={ `${componentId}-${template?.id}` } + > + { + featureStatus + ? ( +
+ { resolveRibbonLabel(featureStatus) } +
+ ) + : null + } + +
+ +
+
+ + { template?.name } + +
+
+ + +
+ + { template?.description } + +
+
+ { supportedTechnologies?.length > 0 && ( + + { supportedTechnologies.map( + (technology: SupportedTechnologyMetadataInterface) => ( + + ) + ) } + + ) } +
+
+ ); +}; + +export default ApplicationTemplateCard; diff --git a/features/admin.application-templates.v1/components/application-templates-grid.scss b/features/admin.application-templates.v1/components/application-templates-grid.scss new file mode 100644 index 00000000000..e84a33f5dd2 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-templates-grid.scss @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +.application-template-card-group { + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: flex-start; + gap: 25px; + margin-bottom: 30px; + + .application-template-card-group-description { + margin-top: -20px; + } +} diff --git a/features/admin.application-templates.v1/components/application-templates-grid.tsx b/features/admin.application-templates.v1/components/application-templates-grid.tsx new file mode 100644 index 00000000000..64d663071d2 --- /dev/null +++ b/features/admin.application-templates.v1/components/application-templates-grid.tsx @@ -0,0 +1,426 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import Typography from "@oxygen-ui/react/Typography"; +import { InboundProtocolsMeta } from "@wso2is/admin.applications.v1/components/meta"; +import { AuthProtocolMetaListItemInterface } from "@wso2is/admin.applications.v1/models"; +import { ApplicationManagementUtils } from "@wso2is/admin.applications.v1/utils/application-management-utils"; +import { AppState, EventPublisher, getEmptyPlaceholderIllustrations } from "@wso2is/admin.core.v1"; +import useExtensionTemplates from "@wso2is/admin.template-core.v1/hooks/use-extension-templates"; +import { + CategorizedExtensionTemplatesInterface, + ExtensionTemplateListInterface +} from "@wso2is/admin.template-core.v1/models/templates"; +import { IdentifiableComponentInterface, LoadableComponentInterface } from "@wso2is/core/models"; +import { + ContentLoader, + DocumentationLink, + EmptyPlaceholder, + GridLayout, + ResourceGrid, + SearchWithFilterLabels, + useDocumentation +} from "@wso2is/react-components"; +import union from "lodash-es/union"; +import React, { FunctionComponent, MouseEvent, ReactElement, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import ApplicationTemplateCard from "./application-template-card"; +import { ApplicationTemplateConstants } from "../constants/templates"; +import { ApplicationTemplateCategories } from "../models/templates"; +import "./application-templates-grid.scss"; + +/** + * Props for the Application templates grid page. + */ +export interface ApplicationTemplateGridPropsInterface extends + IdentifiableComponentInterface, LoadableComponentInterface { + /** + * Callback to be fired when a template is selected. + */ + onTemplateSelect: (template: ExtensionTemplateListInterface) => void; +} + +/** + * Application templates grid page. + * + * @param props - Props injected to the component. + * + * @returns Application template select page. + */ +const ApplicationTemplateGrid: FunctionComponent = ({ + onTemplateSelect, + ["data-componentid"]: componentId = "application-template-grid" +}: ApplicationTemplateGridPropsInterface): ReactElement => { + + const { t } = useTranslation(); + const { getLink } = useDocumentation(); + + const customInboundProtocols: AuthProtocolMetaListItemInterface[] = useSelector((state: AppState) => + state?.application?.meta?.customInboundProtocols); + const hiddenApplicationTemplates: string[] = useSelector((state: AppState) => + state?.config?.ui?.hiddenApplicationTemplates); + + const { + templates, + categorizedTemplates, + isExtensionTemplatesRequestLoading: isApplicationTemplatesRequestLoading + } = useExtensionTemplates(); + + const [ searchQuery, setSearchQuery ] = useState(""); + const [ selectedFilters, setSelectedFilters ] = useState([]); + const [ showCustomProtocolApplicationTemplate, setShowCustomProtocolApplicationTemplate ] = + useState(false); + + const eventPublisher: EventPublisher = EventPublisher.getInstance(); + + /** + * Fetch the custom inbound protocols. + */ + useEffect(() => { + ApplicationManagementUtils.getCustomInboundProtocols(InboundProtocolsMeta, true); + }, []); + + /** + * Show/hide custom application template based on the availability of custom inbound authenticators. + */ + useEffect(() => { + setShowCustomProtocolApplicationTemplate(customInboundProtocols?.length > 0); + }, [ customInboundProtocols ]); + + /** + * Retrieve the filter tags from the `tags` attribute of the application templates. + */ + const filterTags: string[] = useMemo(() => { + if (!templates || !Array.isArray(templates) || templates?.length <= 0) { + return []; + } + + let tags: string[] = []; + + templates.forEach((template: ExtensionTemplateListInterface) => { + tags = union(tags, template?.tags); + }); + + return tags; + }, [ templates ]); + + /** + * Get search results based on the selected tags and the search query. + * + * @param query - Search query. + * @param filterLabels - Array of filter labels. + * + * @returns List of filtered application templates for the provided filter tags and search query. + */ + const getSearchResults = (query: string, filterLabels: string[]): ExtensionTemplateListInterface[] => { + + /** + * Checks if any of the filters are matching. + * + * @param template - Application template object. + * @returns Boolean value indicating whether the filters are matched or not. + */ + const isFiltersMatched = (template: ExtensionTemplateListInterface): boolean => { + + if (!filterLabels || !Array.isArray(filterLabels) || filterLabels?.length <= 0) { + return true; + } + + return template?.tags + ?.some((tagLabel: string) => filterLabels.includes(tagLabel)); + }; + + return templates?.filter((template: ExtensionTemplateListInterface) => { + if (!query) { + return isFiltersMatched(template); + } + + const name: string = template?.name?.toLocaleLowerCase(); + + if (name?.includes(query.toLocaleLowerCase()) + || template?.tags?.some( + (tag: string) => tag?.toLocaleLowerCase()?.includes(query.toLocaleLowerCase())) + ) { + + return isFiltersMatched(template); + } + }); + }; + + /** + * Exclude the application templates that should not be displayed on the application grid page. + * + * templates - Application templates list. + * @returns Filtered application templates list. + */ + const removeIrrelevantTemplates = (templates: ExtensionTemplateListInterface[]) => { + let removingApplicationTemplateIds: string[] = []; + + // Remove custom protocol application templates if there are no custom inbound protocols. + if (!showCustomProtocolApplicationTemplate) { + removingApplicationTemplateIds.push(ApplicationTemplateConstants.CUSTOM_PROTOCOL_APPLICATION_TEMPLATE_ID); + } + + // Remove hidden application templates based on the UI config. + removingApplicationTemplateIds = union(removingApplicationTemplateIds, hiddenApplicationTemplates); + + return templates?.filter( + (template: ExtensionTemplateListInterface) => !removingApplicationTemplateIds.includes(template?.id)); + }; + + /** + * Filter out the application templates based on the selected tags and the search query. + */ + const filteredTemplates: ExtensionTemplateListInterface[] = useMemo(() => { + if (!templates || !Array.isArray(templates) || templates?.length <= 0) { + return []; + } + + if (!searchQuery && (!selectedFilters || !Array.isArray(selectedFilters) || selectedFilters?.length <= 0)) { + return removeIrrelevantTemplates(templates); + } + + return removeIrrelevantTemplates(getSearchResults(searchQuery, selectedFilters)); + }, [ templates, selectedFilters, searchQuery ]); + + /** + * Handles application template selection. + * + * @param e - Click event. + * @param id - Selected template details. + */ + const handleTemplateSelection = ( + e: MouseEvent, + template: ExtensionTemplateListInterface + ): void => { + if (!template) { + return; + } + + eventPublisher.publish("application-click-create-new", { + source: "application-listing-page", + type: template?.id + }); + + onTemplateSelect(template); + }; + + /** + * Handles the Application Template Search input onchange. + * + * @param query - Search query. + * @param selectedFilters - Array of selected filters. + */ + const handleApplicationTemplateSearch = (query: string, selectedFilters: string[]): void => { + + // Update the internal state to manage placeholders etc. + setSearchQuery(query); + setSelectedFilters(selectedFilters); + }; + + /** + * Handles Application Template Type filter. + * + * @param query - Search query. + * @param selectedFilters - Array of the selected filters. + */ + const handleApplicationTemplateTypeFilter = (query: string, selectedFilters: string[]): void => { + + // Update the internal state to manage placeholders etc. + setSearchQuery(query); + setSelectedFilters(selectedFilters); + }; + + /** + * Resolve the relevant placeholder. + * + * list - Application templates list. + * @returns Corresponding placeholder component. + */ + const showPlaceholders = (list: any[]): ReactElement => { + + // When the search returns empty. + if (searchQuery) { + return ( + + ); + } + + // Edge case, templates will never be empty. + if (list?.length === 0) { + return ( + + ); + } + + return null; + }; + + /** + * Resolve the correct documentation link based on the provided category ID. + * + * @param category - The category ID requires the documentation link. + * @returns Documentation link. + */ + const resolveDocumentationLinks = (category: string) => { + switch(category) { + case ApplicationTemplateCategories.DEFAULT: + return getLink("develop.applications.template." + + "categories.default.learnMore"); + case ApplicationTemplateCategories.SSO_INTEGRATION: + return getLink("develop.applications.template." + + "categories.ssoIntegration.learnMore"); + default: + return null; + } + }; + + return ( + + ) } + isLoading={ isApplicationTemplatesRequestLoading } + > + { + (categorizedTemplates && filteredTemplates && !isApplicationTemplatesRequestLoading) + ? ( + searchQuery + || (selectedFilters && Array.isArray(selectedFilters) + && selectedFilters?.length > 0) + ? ( + + { + filteredTemplates + .map((template: ExtensionTemplateListInterface) => { + return ( + ) => { + handleTemplateSelection(e, template); + } } + template={ template } + /> + ); + }) + } + + ) + : ( + (Array.isArray(categorizedTemplates) && categorizedTemplates?.length === 0) + ? showPlaceholders(categorizedTemplates) + : categorizedTemplates + .map((category: CategorizedExtensionTemplatesInterface) => { + const refinedTemplates: ExtensionTemplateListInterface[] = + removeIrrelevantTemplates(category?.templates); + + if (refinedTemplates?.length <= 0) { + return null; + } + + return ( +
+ + { t(category?.displayName) } + + { + category?.description + ? ( + + { t(category?.description) } + + { t("common:learnMore") } + + + ) + : null + } + + { + refinedTemplates.map( + (template: ExtensionTemplateListInterface) => { + return ( + ) => { + handleTemplateSelection( + e, template); + } + } + template={ template } + /> + ); + }) + } + +
+ ); + }) + ) + ) + : + } +
+ ); +}; + +export default ApplicationTemplateGrid; diff --git a/features/admin.application-templates.v1/configs/endpoints.ts b/features/admin.application-templates.v1/configs/endpoints.ts new file mode 100644 index 00000000000..8ee17a9cdd7 --- /dev/null +++ b/features/admin.application-templates.v1/configs/endpoints.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ApplicationsTemplatesEndpointsInterface } from "../models/endpoints"; + +/** + * Get the resource endpoints for the Application Templates Management feature. + * + * @param serverHost - Server Host. + * @returns The resource endpoints for the Application Templates Management feature. + */ +export const getApplicationTemplatesResourcesEndpoints = ( + serverHost: string +): ApplicationsTemplatesEndpointsInterface => { + return { + applicationTemplate: `${serverHost}/api/server/v1/extensions/applications/{{id}}/template`, + applicationTemplateMetadata: `${serverHost}/api/server/v1/extensions/applications/{{id}}/metadata` + }; +}; diff --git a/features/admin.application-templates.v1/constants/templates.ts b/features/admin.application-templates.v1/constants/templates.ts new file mode 100644 index 00000000000..2395b868f42 --- /dev/null +++ b/features/admin.application-templates.v1/constants/templates.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExtensionTemplateCategoryInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { AuthProtocolTypes } from "../../admin.connections.v1"; + +/** + * Class containing application templates management constants. + */ +export class ApplicationTemplateConstants { + public static readonly SUPPORTED_CATEGORIES_INFO: ExtensionTemplateCategoryInterface[] = [ + { + description: "applicationTemplates:categories.default.description", + displayName: "applicationTemplates:categories.default.displayName", + displayOrder: 0, + id: "DEFAULT" + }, + { + description: "applicationTemplates:categories.ssoIntegration.description", + displayName: "applicationTemplates:categories.ssoIntegration.displayName", + displayOrder: 1, + id: "SSO-INTEGRATION" + } + ]; + + public static readonly FEATURE_STATUS_ATTRIBUTE_KEY: string = "featureStatus"; + + public static readonly SUPPORTED_TECHNOLOGIES_ATTRIBUTE_KEY: string = "supportedTechnologies"; + + public static readonly CUSTOM_PROTOCOL_APPLICATION_TEMPLATE_ID: string = "custom-protocol-application"; + + public static readonly APPLICATION_CREATE_WIZARD_SHARING_FIELD_NAME: string = "isApplicationSharable"; + + public static readonly APPLICATION_INBOUND_PROTOCOL_KEYS: { + [ AuthProtocolTypes.WS_FEDERATION ]: string; + [ AuthProtocolTypes.WS_TRUST ]: string; + } = { + [ AuthProtocolTypes.WS_FEDERATION ]: "passiveSts", + [ AuthProtocolTypes.WS_TRUST ]: "wsTrust" + }; +} diff --git a/features/admin.application-templates.v1/context/application-template-context.tsx b/features/admin.application-templates.v1/context/application-template-context.tsx new file mode 100644 index 00000000000..988d6cc4327 --- /dev/null +++ b/features/admin.application-templates.v1/context/application-template-context.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Context, createContext } from "react"; +import { ApplicationTemplateInterface } from "../models/templates"; + +/** + * Props interface for ApplicationTemplateContext. + */ +export interface ApplicationTemplateContextProps { + /** + * Application Template. + */ + template: ApplicationTemplateInterface; + /** + * Flag to determine if the application template is being loaded. + */ + isTemplateRequestLoading: boolean; +} + +/** + * Context object for managing application template. + */ +const ApplicationTemplateContext: Context = + createContext(null); + +/** + * Display name for the ApplicationTemplateContext. + */ +ApplicationTemplateContext.displayName = "ApplicationTemplateContext"; + +export default ApplicationTemplateContext; diff --git a/features/admin.application-templates.v1/context/application-template-metadata-context.tsx b/features/admin.application-templates.v1/context/application-template-metadata-context.tsx new file mode 100644 index 00000000000..f41de30bb9f --- /dev/null +++ b/features/admin.application-templates.v1/context/application-template-metadata-context.tsx @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Context, createContext } from "react"; +import { ApplicationTemplateMetadataInterface } from "../models/templates"; + +/** + * Props interface for ApplicationTemplateMetadataContext. + */ +export interface ApplicationTemplateMetadataContextProps { + /** + * Application Template Metadata. + */ + templateMetadata: ApplicationTemplateMetadataInterface; + /** + * Flag to determine if the application template metadata is being loaded. + */ + isTemplateMetadataRequestLoading: boolean; +} + +/** + * Context object for managing application template metadata. + */ +const ApplicationTemplateMetadataContext: Context = + createContext(null); + +/** + * Display name for the ApplicationTemplateMetadataContext. + */ +ApplicationTemplateMetadataContext.displayName = "ApplicationTemplateMetadataContext"; + +export default ApplicationTemplateMetadataContext; diff --git a/features/admin.application-templates.v1/hooks/use-application-name-validation.tsx b/features/admin.application-templates.v1/hooks/use-application-name-validation.tsx new file mode 100644 index 00000000000..96aae82bfeb --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-application-name-validation.tsx @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getApplicationList } from "@wso2is/admin.applications.v1/api"; +import { ApplicationManagementConstants } from "@wso2is/admin.applications.v1/constants"; +import { ApplicationListInterface } from "@wso2is/admin.applications.v1/models"; +import { AppState } from "@wso2is/admin.core.v1"; +import { AlertLevels } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import { AxiosError } from "axios"; +import debounce, { DebouncedFunc } from "lodash-es/debounce"; +import { MutableRefObject, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { Dispatch } from "redux"; + +/** + * Hook for validate the application name. + * + * @returns The application name validation function. + */ +const useApplicationNameValidation = (): { + validateApplicationName: (name: string, id: string) => Promise +} => { + const { t } = useTranslation(); + + const dispatch: Dispatch = useDispatch(); + + const reservedAppNamePattern: string = useSelector((state: AppState) => { + return state?.config?.deployment?.extensions?.asgardeoReservedAppRegex as string; + }); + + const previouslyValidatedApplicationName: MutableRefObject = useRef(null); + const [ isApplicationNameAlreadyReserved, setIsApplicationNameAlreadyReserved ] = useState(false); + + /** + * Search for applications and retrieve a list for the given app name. + * + * @param appName - Name of the application for searching. + * @returns List of applications found based on the given name. + */ + const getApplications = (appName: string): Promise => { + + return getApplicationList(null, null, "name eq " + appName?.trim()) + .then((response: ApplicationListInterface) => response) + .catch((error: AxiosError) => { + if (error?.response?.data?.description) { + dispatch(addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications.fetchApplications.error.message") + })); + + return null; + } + + dispatch(addAlert({ + description: t("applications:notifications." + + "fetchApplications.genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications." + + "fetchApplications.genericError.message") + })); + + return null; + }); + }; + + /** + * Checks whether the application name is reserved. + * + * @param name - Name of the application. + */ + const isAppNameReserved = (name: string) => { + if(!reservedAppNamePattern) { + return false; + } + const reservedAppRegex: RegExp = new RegExp(reservedAppNamePattern); + + return name && reservedAppRegex.test(name); + }; + + /** + * Checks whether the application name is valid. + * + * @param name - Name of the application. + */ + const isNameValid = (name: string) => { + return name && !!name?.match(ApplicationManagementConstants.FORM_FIELD_CONSTRAINTS.APP_NAME_PATTERN); + }; + + /** + * Check if there is any application with the given name. + * + * @param name - Name of the application. + * @param appId - application id. + */ + const isApplicationNameAlreadyExist: DebouncedFunc<(name: string, appId: string) => Promise> = debounce( + async (name: string, appId: string) => { + if (previouslyValidatedApplicationName?.current !== name) { + previouslyValidatedApplicationName.current = name; + + const response: ApplicationListInterface = await getApplications(name); + + setIsApplicationNameAlreadyReserved( + response?.totalResults > 0 && response?.applications[0]?.id !== appId); + } + }, + 500 + ); + + /** + * Checks whether the application name is valid. + * + * @param name - The value need to be validated. + * @returns Whether the provided value is a valid application name. + */ + const validateApplicationName = async (name: string, id: string): Promise => { + if (!isNameValid(name)) { + return t("applications:forms." + + "spaProtocolSettingsWizard.fields.name.validations.invalid", { + characterLimit: ApplicationManagementConstants.FORM_FIELD_CONSTRAINTS.APP_NAME_MAX_LENGTH, + name + }); + } + + if (isAppNameReserved(name)) { + return t("applications:forms.generalDetails.fields.name.validations.reserved", { + name + }); + } + + await isApplicationNameAlreadyExist(name, id); + + if (isApplicationNameAlreadyReserved) { + return t("applications:forms.generalDetails.fields.name.validations.duplicate"); + } + + return null; + }; + + return { + validateApplicationName + }; +}; + +export default useApplicationNameValidation; diff --git a/features/admin.application-templates.v1/hooks/use-application-template-metadata.ts b/features/admin.application-templates.v1/hooks/use-application-template-metadata.ts new file mode 100644 index 00000000000..77435365c16 --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-application-template-metadata.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useContext } from "react"; +import ApplicationTemplateMetadataContext, { + ApplicationTemplateMetadataContextProps +} from "../context/application-template-metadata-context"; + +/** + * Interface for the return type of the `useApplicationTemplateMetadata` hook. + */ +export type UseApplicationTemplateMetadataInterface = ApplicationTemplateMetadataContextProps; + +/** + * Hook that provides access to the application template metadata context. + * @returns An object containing the application template metadata. + */ +const useApplicationTemplateMetadata = (): UseApplicationTemplateMetadataInterface => { + const context: ApplicationTemplateMetadataContextProps = useContext(ApplicationTemplateMetadataContext); + + if (context === undefined) { + throw new Error( + "useApplicationTemplateMetadata hook must be used within a ApplicationTemplateMetadataProvider"); + } + + return context; +}; + +export default useApplicationTemplateMetadata; diff --git a/features/admin.application-templates.v1/hooks/use-application-template.ts b/features/admin.application-templates.v1/hooks/use-application-template.ts new file mode 100644 index 00000000000..f8d070b8bea --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-application-template.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useContext } from "react"; +import ApplicationTemplateContext, { ApplicationTemplateContextProps } from "../context/application-template-context"; + +/** + * Interface for the return type of the `useApplicationTemplate` hook. + */ +export type UseApplicationTemplateInterface = ApplicationTemplateContextProps; + +/** + * Hook that provides access to the application template context. + * @returns An object containing the application template data. + */ +const useApplicationTemplate = (): UseApplicationTemplateInterface => { + const context: ApplicationTemplateContextProps = useContext(ApplicationTemplateContext); + + if (context === undefined) { + throw new Error("useApplicationTemplate hook must be used within a ApplicationTemplateProvider"); + } + + return context; +}; + +export default useApplicationTemplate; diff --git a/features/admin.application-templates.v1/hooks/use-custom-initialize-handlers.tsx b/features/admin.application-templates.v1/hooks/use-custom-initialize-handlers.tsx new file mode 100644 index 00000000000..aa73d4c1834 --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-custom-initialize-handlers.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CustomInitializeFunction } from + "@wso2is/admin.template-core.v1/hooks/use-initialize-handlers"; +import { + DynamicFieldHandlerInterface, + DynamicFieldInterface +} from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import get from "lodash-es/get"; +import useUniqueApplicationName from "./use-unique-application-name"; +import { ApplicationTemplateInitializeHandlers } from "../models/dynamic-fields"; + +/** + * Hook for custom initialize handlers. + * + * @returns Custom initialize functions. + */ +const useInitializeHandlers = (): { customInitializers: CustomInitializeFunction } => { + + const { generateUniqueApplicationName } = useUniqueApplicationName(); + + /** + * Custom initializer functions to initialize the field based on the handler. + * + * @param formValues - The form values to be initialized. + * @param field - Metadata of the form field. + * @param handler - Handler definition. + * @param templatePayload - Template payload values. + */ + const customInitializers = async ( + formValues: Record, + field: DynamicFieldInterface, + handler: DynamicFieldHandlerInterface, + templatePayload: Record + ): Promise => { + switch (handler?.name) { + case ApplicationTemplateInitializeHandlers.UNIQUE_APPLICATION_NAME: + await generateUniqueApplicationName( + get(templatePayload, field?.name)?.toString()?.trim(), + formValues, + field?.name + ); + + break; + } + }; + + return { customInitializers }; +}; + +export default useInitializeHandlers; diff --git a/features/admin.application-templates.v1/hooks/use-custom-submission-handlers.tsx b/features/admin.application-templates.v1/hooks/use-custom-submission-handlers.tsx new file mode 100644 index 00000000000..fa724533f83 --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-custom-submission-handlers.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CustomSubmissionFunction } from + "@wso2is/admin.template-core.v1/hooks/use-submission-handlers"; +import { + DynamicFieldHandlerInterface, + DynamicFieldInterface +} from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import { ApplicationTemplateSubmissionHandlers } from "../models/dynamic-fields"; +import buildCallBackUrlsWithRegExp from "../utils/build-callback-urls-with-regexp"; + +/** + * Hook for custom submission handlers. + * + * @returns Custom submission handler functions. + */ +const useSubmissionHandlers = (): { customSubmissionHandlers: CustomSubmissionFunction } => { + + /** + * Custom submission handler functions to modify the fields. + * + * @param formValues - The form values to be handled by submission handlers. + * @param field - Metadata of the form field. + * @param handler - Handler definition. + * @param templatePayload - Template payload values. + */ + const customSubmissionHandlers = async ( + formValues: Record, + field: DynamicFieldInterface, + handler: DynamicFieldHandlerInterface, + _templatePayload: Record + ): Promise => { + switch (handler?.name) { + case ApplicationTemplateSubmissionHandlers.BUILD_CALLBACK_URLS_WITH_REGEXP: + buildCallBackUrlsWithRegExp(formValues, field?.name); + + break; + } + }; + + return { customSubmissionHandlers }; +}; + +export default useSubmissionHandlers; diff --git a/features/admin.application-templates.v1/hooks/use-custom-validation-handlers.tsx b/features/admin.application-templates.v1/hooks/use-custom-validation-handlers.tsx new file mode 100644 index 00000000000..ab43c6f6f40 --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-custom-validation-handlers.tsx @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CustomValidationsFunction } from + "@wso2is/admin.template-core.v1/hooks/use-validation-handlers"; +import { + DynamicFieldHandlerInterface, + DynamicFieldInterface +} from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import get from "lodash-es/get"; +import useApplicationNameValidation from "./use-application-name-validation"; +import { ApplicationTemplateValidationHandlers } from "../models/dynamic-fields"; + +/** + * Hook for custom validation handlers. + * + * @returns Custom validation functions. + */ +const useValidationHandlers = (): { customValidations: CustomValidationsFunction } => { + + const { validateApplicationName } = useApplicationNameValidation(); + + /** + * Custom validation function to validate the field based on the handler. + * + * @param formValues - The form values to be validated. + * @param field - Metadata of the form field. + * @param handler - Handler definition. + * @returns An error message if validation fails, or `null` if validation succeeds. + */ + const customValidations = async ( + formValues: Record, + field: DynamicFieldInterface, + handler: DynamicFieldHandlerInterface + ): Promise => { + let validationResult: string = null; + + switch (handler?.name) { + case ApplicationTemplateValidationHandlers.APPLICATION_NAME: + validationResult = await validateApplicationName( + get(formValues, field?.name)?.toString()?.trim(), + get(formValues, "id") as string + ); + + break; + } + + return validationResult; + }; + + return { customValidations }; +}; + +export default useValidationHandlers; diff --git a/features/admin.application-templates.v1/hooks/use-unique-application-name.tsx b/features/admin.application-templates.v1/hooks/use-unique-application-name.tsx new file mode 100644 index 00000000000..7a642f359f8 --- /dev/null +++ b/features/admin.application-templates.v1/hooks/use-unique-application-name.tsx @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { getApplicationList } from "@wso2is/admin.applications.v1/api"; +import { ApplicationListInterface, ApplicationListItemInterface } from "@wso2is/admin.applications.v1/models"; +import set from "lodash-es/set"; +import { MutableRefObject, useRef } from "react"; + +/** + * Interface for duplicate application list cache. + */ +interface DuplicateApplicationListCache { + /** + * Application list response for recently searched application name. + */ + appList: ApplicationListInterface; + /** + * Recently searched application name. + */ + appName: string; +} + +/** + * Hook to generate a unique application name. + * + * @returns The function to generate unique application names. + */ +const useUniqueApplicationName = (): { + generateUniqueApplicationName: ( + initialApplicationName: string, + formValues: Record, + fieldName: string + ) => Promise +} => { + const duplicateApplicationListCache: MutableRefObject = + useRef(null); + + /** + * Generate the next unique name by appending 1-based index number to the provided initial value. + * + * @param initialApplicationName - Initial value for the Application name. + * @returns A unique name from the provided list of names. + */ + const generateUniqueApplicationName = async ( + initialApplicationName: string, + formValues: Record, + fieldName: string + ): Promise => { + + let appName: string = initialApplicationName?.trim(); + let possibleListOfDuplicateApplications: ApplicationListInterface = + duplicateApplicationListCache?.current?.appList; + + if (duplicateApplicationListCache?.current?.appName !== appName) { + possibleListOfDuplicateApplications = await getApplicationList(null, null, "name sw " + appName); + duplicateApplicationListCache.current = { + appList: possibleListOfDuplicateApplications, + appName + }; + } + + if (possibleListOfDuplicateApplications?.totalResults > 0) { + const applicationNameList: string[] = possibleListOfDuplicateApplications?.applications?.map( + (item: ApplicationListItemInterface) => item?.name); + + for (let i: number = 2; ; i++) { + if (!applicationNameList?.includes(appName)) { + break; + } + + appName = initialApplicationName + " " + i; + } + } + + set(formValues, fieldName, appName); + }; + + return { generateUniqueApplicationName }; +}; + +export default useUniqueApplicationName; diff --git a/features/admin.application-templates.v1/models/dynamic-fields.ts b/features/admin.application-templates.v1/models/dynamic-fields.ts new file mode 100644 index 00000000000..29c09fab08f --- /dev/null +++ b/features/admin.application-templates.v1/models/dynamic-fields.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Supported custom validation handlers for application templates. + */ +export enum ApplicationTemplateValidationHandlers { + APPLICATION_NAME = "applicationName", +} + +/** + * Supported custom initialize handlers for application templates. + */ +export enum ApplicationTemplateInitializeHandlers { + UNIQUE_APPLICATION_NAME = "uniqueApplicationName", +} + +/** + * Supported custom submission handlers for application templates. + */ +export enum ApplicationTemplateSubmissionHandlers { + BUILD_CALLBACK_URLS_WITH_REGEXP = "buildCallbackURLsWithRegexp", +} diff --git a/features/admin.application-templates.v1/models/endpoints.ts b/features/admin.application-templates.v1/models/endpoints.ts new file mode 100644 index 00000000000..3bddeddeb8f --- /dev/null +++ b/features/admin.application-templates.v1/models/endpoints.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Interface for the Application Templates Management feature resource endpoints. + */ +export interface ApplicationsTemplatesEndpointsInterface { + /** + * Endpoint to get the application template metadata. + */ + applicationTemplateMetadata: string; + /** + * Endpoint to get the application template. + */ + applicationTemplate: string; +} diff --git a/features/admin.application-templates.v1/models/templates.ts b/features/admin.application-templates.v1/models/templates.ts new file mode 100644 index 00000000000..41dd3955478 --- /dev/null +++ b/features/admin.application-templates.v1/models/templates.ts @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MainApplicationInterface } from "@wso2is/admin.applications.v1/models"; +import { DynamicFormInterface } from "@wso2is/admin.template-core.v1/models/dynamic-fields"; +import { ExtensionTemplateCommonInterface } from "@wso2is/admin.template-core.v1/models/templates"; + +/** + * Interface for the application template. + */ +export interface ApplicationTemplateInterface extends ExtensionTemplateCommonInterface { + /** + * Create form payload parameters. + */ + payload: MainApplicationInterface; +} + +/** + * Supported technology metadata interface. + */ +export interface SupportedTechnologyMetadataInterface { + /** + * Display name of the technology. + */ + displayName: string; + /** + * URL of the technology logo. + */ + logo?: string; +} + +/** + * Interface for the application template metadata. + */ +export interface ApplicationTemplateMetadataInterface { + /** + * Application creation related metadata. + */ + create?: { + /** + * Dynamic input fields should be rendered in the application create wizard. + */ + form?: DynamicFormInterface; + /** + * Application creation guide metadata. + */ + guide?: string[]; + } + /** + * Application editing section related metadata. + */ + edit?: { + /** + * The metadata for tabs needs to be rendered on the edit page. + */ + tabs: ApplicationEditTabMetadataInterface[], + /** + * Tab id of the default active tab. + */ + defaultActiveTabId?: string; + } +} + +/** + * Possible Content Types for application editing tabs. + */ +export enum ApplicationEditTabContentType { + FORM = "form", + GUIDE = "guide" +} + +/** + * Interface to generate a tab in the application editing section. + */ +export interface ApplicationEditTabMetadataInterface { + /** + * Unique identifier for the tab. + */ + id: string; + /** + * Display name of the tab. + */ + displayName?: string; + /** + * Content Types for current tab. + */ + contentType?: ApplicationEditTabContentType; + /** + * Dynamic input fields which should be rendered in the current tab. + */ + form?: DynamicFormInterface; + /** + * Guide content for application editing section. + */ + guide?: string; + /** + * Component IDs that need to be hidden from a predefined tab. + * This is only effective if the `contentType` is not defined. + */ + hiddenComponents?: string[]; +} + +/** + * Enum for application template categories. + * + * @readonly + */ +export enum ApplicationTemplateCategories { + /** + * Templates supported by default. + * ex: Web Application, SPA etc. + */ + DEFAULT = "DEFAULT", + /** + * SSO Integration templates. + * ex: Zoom, Salesforce etc. + */ + SSO_INTEGRATION = "SSO-INTEGRATION", +} + +/** + * Supported application feature status list. + */ +export enum ApplicationTemplateFeatureStatus { + NEW = "new", + COMING_SOON = "comingSoon" +} diff --git a/features/admin.application-templates.v1/package.json b/features/admin.application-templates.v1/package.json new file mode 100644 index 00000000000..6781f7c6082 --- /dev/null +++ b/features/admin.application-templates.v1/package.json @@ -0,0 +1,38 @@ +{ + "private": true, + "name": "@wso2is/admin.application-templates.v1", + "version": "0.0.0", + "description": "WSO2 Identity Server Console", + "author": "WSO2", + "license": "Apache-2.0", + "dependencies": { + "@wso2is/admin.applications.v1": "^2.21.11", + "@wso2is/admin.core.v1": "^2.21.11", + "@wso2is/admin.template-core.v1": "^1.0.0" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-dynamic-import-vars": "^2.1.2", + "@rollup/plugin-image": "^3.0.3", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@svgr/rollup": "^6.2.1", + "rollup": "^4.17.2", + "rollup-plugin-dts": "^6.1.1", + "rollup-plugin-generate-package-json": "^3.2.0", + "rollup-plugin-polyfill-node": "^0.13.0", + "rollup-plugin-scss": "^4.0.0", + "rollup-plugin-styles": "^4.0.0", + "rollup-plugin-svg": "^2.0.0", + "typescript": "^4.6.4" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^4.3.1" + }, + "browserslist": [ + "> 0.2%" + ] +} diff --git a/features/admin.application-templates.v1/pages/application-template.tsx b/features/admin.application-templates.v1/pages/application-template.tsx new file mode 100755 index 00000000000..6dfd3be83e4 --- /dev/null +++ b/features/admin.application-templates.v1/pages/application-template.tsx @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AppConstants, history } from "@wso2is/admin.core.v1"; +import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { TestableComponentInterface } from "@wso2is/core/models"; +import { PageLayout } from "@wso2is/react-components"; +import React, { FunctionComponent, ReactElement, useState } from "react"; +import { useTranslation } from "react-i18next"; +import ApplicationCreationAdapter from "../components/application-creation-adapter"; +import ApplicationTemplateGrid from "../components/application-templates-grid"; +import ApplicationTemplateMetadataProvider from + "../provider/application-template-metadata-provider"; +import ApplicationTemplateProvider from "../provider/application-template-provider"; + +/** + * Props for the Applications templates page. + */ +type ApplicationTemplateSelectPageInterface = TestableComponentInterface; + +/** + * Choose the application template from this page. + * + * @param props - Props injected to the component. + * + * @returns Application template select page. + */ +const ApplicationTemplateSelectPage: FunctionComponent = ({ + "data-testid": testId = "application-templates" +}: ApplicationTemplateSelectPageInterface): ReactElement => { + + const { t } = useTranslation(); + + const [ showWizard, setShowWizard ] = useState(false); + const [ selectedTemplate, setSelectedTemplate ] = useState(null); + + /** + * Handles back button click. + */ + const handleBackButtonClick = (): void => { + history.push(AppConstants.getPaths().get("APPLICATIONS")); + }; + + return ( + + { + setSelectedTemplate(template); + setShowWizard(true); + } } + /> + + + setShowWizard(false) } + /> + + + + ); +}; + +/** + * A default export was added to support React.lazy. + * TODO: Change this to a named export once react starts supporting named exports for code splitting. + * @see {@link https://reactjs.org/docs/code-splitting.html#reactlazy} + */ +export default ApplicationTemplateSelectPage; diff --git a/features/admin.application-templates.v1/provider/application-template-metadata-provider.tsx b/features/admin.application-templates.v1/provider/application-template-metadata-provider.tsx new file mode 100644 index 00000000000..f3f3f07a1f3 --- /dev/null +++ b/features/admin.application-templates.v1/provider/application-template-metadata-provider.tsx @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { AlertLevels } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import React, { FunctionComponent, PropsWithChildren, ReactElement, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import useGetApplicationTemplateMetadata from "../api/use-get-application-template-metadata"; +import ApplicationTemplateMetadataContext from "../context/application-template-metadata-context"; + +/** + * Props interface for the Application template metadata provider. + */ +export interface ApplicationTemplateMetadataProviderProps { + /** + * Listing data of the selected template. + */ + template: ExtensionTemplateListInterface +} + +/** + * Application template metadata provider. + * + * @param props - Props for the provider. + * @returns Application template metadata provider. + */ +const ApplicationTemplateMetadataProvider: FunctionComponent< + PropsWithChildren +> = ({ + children, + template +}: PropsWithChildren): ReactElement => { + + const { t } = useTranslation(); + + const dispatch: Dispatch = useDispatch(); + + const { + data: applicationTemplateMetadata, + isLoading: isApplicationTemplateMetadataFetchRequestLoading, + error: applicationTemplateMetadataFetchRequestError + } = useGetApplicationTemplateMetadata(template?.id, !!template?.id); + + /** + * Handle errors that occur during the application template meta data fetch request. + */ + useEffect(() => { + if (!applicationTemplateMetadataFetchRequestError + || applicationTemplateMetadataFetchRequestError?.response?.status === 404) { + return; + } + + if (applicationTemplateMetadataFetchRequestError?.response?.data?.description) { + dispatch(addAlert({ + description: applicationTemplateMetadataFetchRequestError?.response?.data?.description, + level: AlertLevels.ERROR, + message: t("applicationTemplates:notifications.fetchTemplateMetadata.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applicationTemplates:notifications.fetchTemplateMetadata" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applicationTemplates:notifications." + + "fetchTemplateMetadata.genericError.message") + })); + }, [ applicationTemplateMetadataFetchRequestError ]); + + return ( + + { children } + + ); +}; + +export default ApplicationTemplateMetadataProvider; diff --git a/features/admin.application-templates.v1/provider/application-template-provider.tsx b/features/admin.application-templates.v1/provider/application-template-provider.tsx new file mode 100644 index 00000000000..b4f0d0385f7 --- /dev/null +++ b/features/admin.application-templates.v1/provider/application-template-provider.tsx @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { AlertLevels } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; +import React, { FunctionComponent, PropsWithChildren, ReactElement, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; +import useGetApplicationTemplate from "../api/use-get-application-template"; +import ApplicationTemplateContext from "../context/application-template-context"; +import { ApplicationTemplateInterface } from "../models/templates"; + +/** + * Props interface for the Application template provider. + */ +export interface ApplicationTemplateProviderProps { + /** + * Listing data of the selected template. + */ + template: ExtensionTemplateListInterface +} + +/** + * Application template provider. + * + * @param props - Props for the provider. + * @returns Application template provider. + */ +const ApplicationTemplateProvider: FunctionComponent< + PropsWithChildren +> = ({ + children, + template +}: PropsWithChildren): ReactElement => { + + const { t } = useTranslation(); + + const dispatch: Dispatch = useDispatch(); + + const { + data: applicationTemplate, + isLoading: isApplicationTemplateFetchRequestLoading, + error: applicationTemplateFetchRequestError + } = useGetApplicationTemplate(template?.id, !!template?.id); + + /** + * Handle errors that occur during the application template data fetch request. + */ + useEffect(() => { + if (!applicationTemplateFetchRequestError || applicationTemplateFetchRequestError?.response?.status === 404) { + return; + } + + if (applicationTemplateFetchRequestError?.response?.data?.description) { + dispatch(addAlert({ + description: applicationTemplateFetchRequestError?.response?.data?.description, + level: AlertLevels.ERROR, + message: t("applicationTemplates:notifications.fetchTemplate.error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applicationTemplates:notifications.fetchTemplate" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applicationTemplates:notifications." + + "fetchTemplate.genericError.message") + })); + }, [ applicationTemplateFetchRequestError ]); + + /** + * Memoized function to combine template data with its listing data. + */ + const templateData: ApplicationTemplateInterface = useMemo(() => { + if (!applicationTemplate || !template) { + return null; + } + + const { self: _self, customAttributes: _customAttributes, ...rest } = template; + + return { + ...rest, + ...applicationTemplate + }; + }, [ applicationTemplate, template ]); + + return ( + + { children } + + ); +}; + +export default ApplicationTemplateProvider; diff --git a/features/admin.application-templates.v1/public-api.ts b/features/admin.application-templates.v1/public-api.ts new file mode 100644 index 00000000000..82d118f85e6 --- /dev/null +++ b/features/admin.application-templates.v1/public-api.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { default as ApplicationTemplateProvider } from "./provider/application-template-provider"; +export { default as ApplicationTemplateMetadataProvider } from "./provider/application-template-metadata-provider"; +export { default as ApplicationTemplatePage } from "./pages/application-template"; diff --git a/features/admin.application-templates.v1/rollup.config.cjs b/features/admin.application-templates.v1/rollup.config.cjs new file mode 100644 index 00000000000..a64ea2c5a80 --- /dev/null +++ b/features/admin.application-templates.v1/rollup.config.cjs @@ -0,0 +1,74 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const commonjs = require("@rollup/plugin-commonjs"); +const dynamicImportVars = require("@rollup/plugin-dynamic-import-vars"); +const image = require("@rollup/plugin-image"); +const json = require("@rollup/plugin-json"); +const { nodeResolve } = require("@rollup/plugin-node-resolve"); +const typescript = require("@rollup/plugin-typescript"); +const svgr = require("@svgr/rollup"); +const dts = require("rollup-plugin-dts"); +const nodePolyfills = require("rollup-plugin-polyfill-node"); +const scss = require("rollup-plugin-scss"); +const svg = require("rollup-plugin-svg"); + +const onwarn = (warning, warn) => { + if (warning.code === "MODULE_LEVEL_DIRECTIVE") { + return; + } + warn(warning); +}; + +module.exports = [ + { + cache: false, + external: [ "react", "react-dom", /^@wso2is\// ], + input: [ + "./public-api.ts" + ], + onwarn, + output: [ + { + dir: "dist/esm", + format: "esm", + preserveModules: true, + preserveModulesRoot: "." + } + ], + plugins: [ + nodeResolve(), + typescript({ + tsconfig: "./tsconfig.json" + }), + scss(), + svg(), + svgr(), + json(), + image(), + nodePolyfills(), + commonjs(), + dynamicImportVars() + ] + }, + { + cache: false, + input: "dist/esm/types/public-api.d.ts", + output: [ { file: "dist/esm/index.d.ts", format: "esm" } ], + plugins: [ dts.default() ] + } +]; diff --git a/features/admin.application-templates.v1/tsconfig.json b/features/admin.application-templates.v1/tsconfig.json new file mode 100644 index 00000000000..71337b4c236 --- /dev/null +++ b/features/admin.application-templates.v1/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "jsx": "react", + "declaration": true, + "declarationDir": "dist/esm/types", + "lib": [ "ESNext", "DOM", "DOM.Iterable", "ScriptHost" ], + "resolveJsonModule": true, + "skipDefaultLibCheck": true, + "types": [ "node", "webpack-env", "jest", "@testing-library/jest-dom" ], + "emitDecoratorMetadata": true, + "esModuleInterop": true, + "experimentalDecorators": true, + "importHelpers": true, + "module": "esnext", + "moduleResolution": "node", + "skipLibCheck": true, + "sourceMap": true, + "target": "es2015", + }, + "exclude": [ + "build", + "cache", + "coverage", + "dist", + "node_modules", + "scripts", + "**/test-configs/*", + "jest.config.ts", + "**/tests/*", + "**/__tests__/*", + "**/__mocks__/*", + "**/*.test.js", + "**/*.test.jsx", + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.spec.ts", + "**/*.spec.tsx" + ], + "compileOnSave": false, +} diff --git a/features/admin.application-templates.v1/utils/build-callback-urls-with-regexp.ts b/features/admin.application-templates.v1/utils/build-callback-urls-with-regexp.ts new file mode 100644 index 00000000000..9a6814c3cf7 --- /dev/null +++ b/features/admin.application-templates.v1/utils/build-callback-urls-with-regexp.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import get from "lodash-es/get"; + +/** + * Build an array of URLs or a regular expression string from an array of URLs. + * + * @param formValues - Object containing initial values for the form. + * @param fieldName - Path of the field value within the object. + * @returns An array of URLs or a regular expression string. + * + * @example + * // When there is a single URL: + * const url = buildCallBackUrlsWithRegExp(["https://example.com/login"]); + * // Result: ["https://example.com/login"] + * + * // When there are multiple URLs: + * const urls = buildCallBackUrlsWithRegExp(["https://example.com/login", "https://app.example.com/login"]); + * // Result: ["regexp=(https://example.com/login|https://app.example.com/login)"] + */ +const buildCallBackUrlsWithRegExp = (formValues: Record, fieldName: string): string[] => { + const urls: string[] = get(formValues, fieldName) as string[]; + const sanitizedURLs: string[] = urls?.map((url: string) => url?.replace(/['"]+/g, "")) || []; + + if (sanitizedURLs?.length > 1) { + return [ `regexp=(${sanitizedURLs.join("|")})` ]; + } + + return sanitizedURLs; +}; + +export default buildCallBackUrlsWithRegExp; diff --git a/features/admin.applications.v1/components/application-list.tsx b/features/admin.applications.v1/components/application-list.tsx index 468dbdb6f50..46dbe39314b 100644 --- a/features/admin.applications.v1/components/application-list.tsx +++ b/features/admin.applications.v1/components/application-list.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -17,7 +17,7 @@ */ import Grid from "@oxygen-ui/react/Grid"; -import { Show } from "@wso2is/access-control"; +import { Show, useRequiredScopes } from "@wso2is/access-control"; import { ConsoleSettingsModes } from "@wso2is/admin.console-settings.v1/models/ui"; import { AppConstants, @@ -32,7 +32,9 @@ import { applicationConfig } from "@wso2is/admin.extensions.v1"; import { applicationListConfig } from "@wso2is/admin.extensions.v1/configs/application-list"; import { OrganizationType } from "@wso2is/admin.organizations.v1/constants"; import { useGetCurrentOrganizationType } from "@wso2is/admin.organizations.v1/hooks/use-get-organization-type"; -import { hasRequiredScopes, isFeatureEnabled } from "@wso2is/core/helpers"; +import useExtensionTemplates from "@wso2is/admin.template-core.v1/hooks/use-extension-templates"; +import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/models/templates"; +import { isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, IdentifiableComponentInterface, @@ -75,8 +77,8 @@ import { ApplicationTemplateManagementUtils } from "../utils/application-templat * * Proptypes for the applications list component. */ -interface ApplicationListPropsInterface extends SBACInterface, LoadableComponentInterface, - TestableComponentInterface, IdentifiableComponentInterface { +export interface ApplicationListPropsInterface extends SBACInterface, + LoadableComponentInterface, TestableComponentInterface, IdentifiableComponentInterface { /** * Advanced Search component. @@ -161,11 +163,18 @@ export const ApplicationList: FunctionComponent = (state: AppState) => state.application.templates); const groupedApplicationTemplates: ApplicationTemplateListItemInterface[] = useSelector( (state: AppState) => state?.application?.groupedTemplates); - const allowedScopes: string = useSelector((state: AppState) => state?.auth?.allowedScopes); const UIConfig: UIConfigInterface = useSelector((state: AppState) => state?.config?.ui); const tenantDomain: string = useSelector((state: AppState) => state?.auth?.tenantDomain); const { organizationType } = useGetCurrentOrganizationType(); + const { + templates: extensionApplicationTemplates, + isExtensionTemplatesRequestLoading: isExtensionApplicationTemplatesRequestLoading + } = useExtensionTemplates(); + // Check if the user has the required scopes to update the application. + const hasApplicationUpdatePermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.update); + // Check if the user has the required scopes to delete the application. + const hasApplicationDeletePermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.delete); const [ showDeleteConfirmationModal, setShowDeleteConfirmationModal ] = useState(false); const [ deletingApplication, setDeletingApplication ] = useState(undefined); @@ -350,6 +359,18 @@ export const ApplicationList: FunctionComponent = } } + /** + * This condition block will help identify the applications created from templates + * on the extensions management API side. + */ + if (!templateDisplayName) { + templateDisplayName = extensionApplicationTemplates?.find( + (template: ExtensionTemplateListInterface) => { + return template?.id === app?.templateId; + } + )?.name; + } + return (
= ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT")), icon: (app: ApplicationListItemInterface): SemanticICONS => { return app?.access === ApplicationAccessTypes.READ - || !hasRequiredScopes(featureConfig?.applications, - featureConfig?.applications?.scopes?.update, allowedScopes) + || !hasApplicationUpdatePermissions ? "eye" : "pencil alternate"; }, @@ -509,8 +529,7 @@ export const ApplicationList: FunctionComponent = handleApplicationEdit(app.id, app.access, app.name), popupText: (app: ApplicationListItemInterface): string => { return app?.access === ApplicationAccessTypes.READ - || !hasRequiredScopes(featureConfig?.applications, - featureConfig?.applications?.scopes?.update, allowedScopes) + || !hasApplicationUpdatePermissions ? t("common:view") : t("common:edit"); }, @@ -519,13 +538,11 @@ export const ApplicationList: FunctionComponent = { "data-testid": `${ testId }-item-delete-button`, hidden: (app: ApplicationListItemInterface) => { - const hasScopes: boolean = !hasRequiredScopes(featureConfig?.applications, - featureConfig?.applications?.scopes?.delete, allowedScopes); const isSuperTenant: boolean = (tenantDomain === AppConstants.getSuperTenant()); const isSystemApp: boolean = isSuperTenant && (UIConfig.systemAppsIdentifiers.includes(app?.name)); const isFragmentApp: boolean = app.advancedConfigurations?.fragment || false; - return hasScopes || + return !hasApplicationDeletePermissions || isSystemApp || (app?.access === ApplicationAccessTypes.READ) || !applicationConfig.editApplication.showDeleteButton(app) || @@ -608,7 +625,11 @@ export const ApplicationList: FunctionComponent = className="applications-table" externalSearch={ advancedSearch } - isLoading={ isLoading || isApplicationTemplateRequestLoading } + isLoading={ + isLoading + || isApplicationTemplateRequestLoading + || isExtensionApplicationTemplatesRequestLoading + } actions={ !isSetStrongerAuth && resolveTableActions() } columns={ resolveTableColumns() } data={ list?.applications } @@ -619,7 +640,12 @@ export const ApplicationList: FunctionComponent = placeholders={ showPlaceholders() } selectable={ selection } showHeader={ applicationListConfig.enableTableHeaders } - transparent={ !(isLoading || isApplicationTemplateRequestLoading) && (showPlaceholders() !== null) } + transparent={ + !(isLoading + || isApplicationTemplateRequestLoading + || isExtensionApplicationTemplatesRequestLoading) + && (showPlaceholders() !== null) + } data-testid={ testId } /> { diff --git a/features/admin.applications.v1/components/edit-application.tsx b/features/admin.applications.v1/components/edit-application.tsx index fce0424840e..554a442d55d 100644 --- a/features/admin.applications.v1/components/edit-application.tsx +++ b/features/admin.applications.v1/components/edit-application.tsx @@ -17,6 +17,14 @@ */ import { Show, useRequiredScopes } from "@wso2is/access-control"; +import { ApplicationEditForm } from "@wso2is/admin.application-templates.v1/components/application-edit-form"; +import { ApplicationMarkdownGuide } from "@wso2is/admin.application-templates.v1/components/application-markdown-guide"; +import useApplicationTemplateMetadata from + "@wso2is/admin.application-templates.v1/hooks/use-application-template-metadata"; +import { + ApplicationEditTabContentType, + ApplicationEditTabMetadataInterface +} from "@wso2is/admin.application-templates.v1/models/templates"; import { BrandingPreferencesConstants } from "@wso2is/admin.branding.v1/constants"; import { AppConstants, @@ -28,7 +36,7 @@ import { history } from "@wso2is/admin.core.v1"; import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; -import { applicationConfig } from "@wso2is/admin.extensions.v1"; +import { ApplicationTabIDs, applicationConfig } from "@wso2is/admin.extensions.v1"; import { MyAccountOverview } from "@wso2is/admin.extensions.v1/configs/components/my-account-overview"; import AILoginFlowProvider from "@wso2is/admin.login-flow.ai.v1/providers/ai-login-flow-provider"; import { OrganizationType } from "@wso2is/admin.organizations.v1/constants"; @@ -44,11 +52,13 @@ import { DangerZoneGroup, Link, ResourceTab, - ResourceTabPaneInterface + ResourceTabPaneInterface, + TAB_URL_HASH_FRAGMENT } from "@wso2is/react-components"; import Axios, { AxiosError, AxiosResponse } from "axios"; +import cloneDeep from "lodash-es/cloneDeep"; import isEmpty from "lodash-es/isEmpty"; -import React, { FormEvent, FunctionComponent, ReactElement, SyntheticEvent, useEffect, useMemo, useState } from "react"; +import React, { FormEvent, FunctionComponent, ReactElement, SyntheticEvent, useEffect, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; @@ -78,8 +88,7 @@ import { OIDCApplicationConfigurationInterface, OIDCDataInterface, SAMLApplicationConfigurationInterface, - SupportedAuthProtocolTypes, - URLFragmentTypes + SupportedAuthProtocolTypes } from "../models"; import { ApplicationManagementUtils } from "../utils/application-management-utils"; @@ -156,9 +165,15 @@ export const EditApplication: FunctionComponent = } = props; const { t } = useTranslation(); - const { isSuperOrganization } = useGetCurrentOrganizationType(); + const { isSuperOrganization, isSubOrganization } = useGetCurrentOrganizationType(); const dispatch: Dispatch = useDispatch(); const { UIConfig } = useUIConfig(); + const { + templateMetadata: extensionTemplateMetadata, + isTemplateMetadataRequestLoading: isExtensionTemplateMetadataFetchRequestLoading + } = useApplicationTemplateMetadata(); + // Check if the user has the required scopes to update the application. + const hasApplicationUpdatePermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.update); const availableInboundProtocols: AuthProtocolMetaListItemInterface[] = useSelector((state: AppState) => state.application.meta.inboundProtocols); @@ -190,9 +205,6 @@ export const EditApplication: FunctionComponent = ] = useState<{ clientSecret: string; clientId: string }>({ clientId: "", clientSecret: "" }); const [ isOIDCConfigsLoading, setOIDCConfigsLoading ] = useState(false); const [ isSAMLConfigsLoading, setSAMLConfigsLoading ] = useState(false); - const [ activeTabIndex, setActiveTabIndex ] = useState(undefined); - const [ defaultActiveIndex, setDefaultActiveIndex ] = useState(undefined); - const [ totalTabs, setTotalTabs ] = useState(undefined); const [ isM2MApplication, setM2MApplication ] = useState(false); const eventPublisher: EventPublisher = EventPublisher.getInstance(); @@ -204,17 +216,12 @@ export const EditApplication: FunctionComponent = ApplicationManagementConstants.MY_ACCOUNT_APP_NAME === application?.name; const applicationsUpdateScopes: string[] = featureConfig?.applications?.scopes?.update; - const { isSubOrganization } = useGetCurrentOrganizationType(); const [ isDisableInProgress, setIsDisableInProgress ] = useState(false); const [ enableStatus, setEnableStatus ] = useState(false); const [ showDisableConfirmationModal, setShowDisableConfirmationModal ] = useState(false); const brandingDisabledFeatures: string[] = useSelector((state: AppState) => state?.config?.ui?.features?.branding?.disabledFeatures); - const hasApplicationsUpdatePermissions: boolean = useRequiredScopes( - featureConfig?.applications?.scopes?.update - ); - /** * Called when an application updates. * @@ -226,723 +233,164 @@ export const EditApplication: FunctionComponent = }; /** - * Resolves the tab panes based on the application config. - * - * @returns Resolved tab panes. + * Todo Remove this mapping and fix the backend. */ - const resolveTabPanes = (): ResourceTabPaneInterface[] => { - const panes: ResourceTabPaneInterface[] = []; - const extensionPanes: ResourceTabPaneInterface[] = []; + const mapProtocolTypeToName = ((type: string): string => { + let protocolName: string = type; - if (!tabPaneExtensions && applicationConfig.editApplication.extendTabs - && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC - && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS - && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_SAML) { - return []; + if (protocolName === "oauth2") { + protocolName = SupportedAuthProtocolTypes.OIDC; + } else if (protocolName === "passivests") { + protocolName = SupportedAuthProtocolTypes.WS_FEDERATION; + } else if (protocolName === "wstrust") { + protocolName = SupportedAuthProtocolTypes.WS_TRUST; + } else if (protocolName === "samlsso") { + protocolName = SupportedAuthProtocolTypes.SAML; } - if (tabPaneExtensions && tabPaneExtensions.length > 0) { - extensionPanes.push(...tabPaneExtensions); - } + return protocolName; + }); - if (featureConfig) { - if (isMyAccount) { - panes.push({ - componentId: "overview", - menuItem: t("applications:myaccount.overview.tabName"), - render: MyAccountOverviewTabPane - }); - } - if ( - isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_GENERAL_SETTINGS")) - && (isSubOrganization() ? - !brandingDisabledFeatures.includes( - BrandingPreferencesConstants.APP_WISE_BRANDING_FEATURE_TAG): true) - && !isMyAccount - ) { - if (applicationConfig.editApplication. - isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.GENERAL, - tenantDomain - )) { - panes.push({ - componentId: "general", - menuItem: - - { t("applications:edit.sections.general.tabName") } - , - render: () => - applicationConfig.editApplication. - getOveriddenTab( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.GENERAL, - GeneralApplicationSettingsTabPane(), - application?.name, - application?.id, - tenantDomain - ) - }); - } - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ACCESS_CONFIG")) - && !isFragmentApp - && !isMyAccount - ) { + /** + * This function will normalize the SAML name ID format + * returned by the API. + * + * @param protocolConfigs - Protocol config object + */ + const normalizeSAMLNameIDFormat = (protocolConfigs: any): void => { + const key: string = "saml"; - applicationConfig.editApplication.isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.PROTOCOL, - tenantDomain - ) && - panes.push({ - componentId: "protocol", - menuItem: t("applications:edit.sections.access.tabName"), - render: ApplicationSettingsTabPane - }); - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ATTRIBUTE_MAPPING")) - && !isFragmentApp - && !isM2MApplication - && ( - UIConfig?.legacyMode?.applicationSystemAppsSettings || - application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME - ) - ) { + if (protocolConfigs[ key ]) { + const assertion: any = protocolConfigs[ key ].singleSignOnProfile?.assertion; - applicationConfig.editApplication.isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.USER_ATTRIBUTES, tenantDomain) && - panes.push({ - componentId: "user-attributes", - menuItem: - - { t("applications:edit.sections.attributes.tabName") } - , - render: () => - applicationConfig.editApplication. - getOveriddenTab( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.USER_ATTRIBUTES, - AttributeSettingTabPane(), - application?.name, - application?.id, - tenantDomain - ) - }); - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_SIGN_ON_METHOD_CONFIG")) - && !isM2MApplication) { + if (assertion) { + const ref: string = assertion.nameIdFormat as string; - applicationConfig.editApplication. - isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.SIGN_IN_METHOD, tenantDomain) && - panes.push({ - componentId: "sign-in-method", - menuItem: - - { t("applications:edit.sections.signOnMethod.tabName") } - , - render: SignOnMethodsTabPane - }); + assertion.nameIdFormat = ref.replace(/\//g, ":"); } - if (applicationConfig.editApplication.showProvisioningSettings - && isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_PROVISIONING_SETTINGS")) - && !isFragmentApp - && !isM2MApplication - && ( - UIConfig?.legacyMode?.applicationSystemAppsSettings || - application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME - )) { + } + }; - applicationConfig.editApplication.isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.PROVISIONING, tenantDomain) && - panes.push({ - componentId: "provisioning", - menuItem: t("applications:edit.sections.provisioning.tabName"), - render: ProvisioningSettingsTabPane - }); - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ADVANCED_SETTINGS")) - && !isFragmentApp - && !isM2MApplication - && ( - UIConfig?.legacyMode?.applicationSystemAppsSettings || - application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME - ) - ) { + /** + * Finds the configured inbound protocol. + */ + const findConfiguredInboundProtocol = (appId: string): void => { + let protocolConfigs: any = {}; + const selectedProtocolList: string[] = []; + const inboundProtocolRequests: Promise[] = []; + const protocolNames: string[] = []; - applicationConfig.editApplication. - isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId , ApplicationTabTypes.ADVANCED, tenantDomain) && - panes.push({ - componentId: "advanced", - menuItem: ( - - { t("applications:edit.sections.advanced.tabName") } - ), - render: AdvancedSettingsTabPane - }); - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_SHARED_ACCESS")) - && application?.templateId != ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS - && !isFragmentApp - && !isM2MApplication - && applicationConfig.editApplication.showApplicationShare - && (isFirstLevelOrg || window[ "AppUtils" ].getConfig().organizationName) - && hasApplicationsUpdatePermissions - && orgType !== OrganizationType.SUBORGANIZATION - && !ApplicationManagementConstants.SYSTEM_APPS.includes(application?.clientId)) { - applicationConfig.editApplication. - isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.INFO, - tenantDomain - ) && - panes.push({ - componentId: "shared-access", - menuItem: t("applications:edit.sections.sharedAccess.tabName"), - render: SharedAccessTabPane - }); - } - if (isFeatureEnabled(featureConfig?.applications, - ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_INFO")) - && !isFragmentApp - && !isMyAccount) { - applicationConfig.editApplication. - isTabEnabledForApp( - inboundProtocolConfig?.oidc?.clientId, - ApplicationTabTypes.INFO, - tenantDomain - ) && - panes.push({ - componentId: "info", - menuItem: { - content: t("applications:edit.sections.info.tabName"), - icon: "info circle grey" - }, - render: InfoTabPane - }); - } + if (application?.inboundProtocols?.length > 0) { + application.inboundProtocols.forEach((protocol: InboundProtocolListItemInterface) => { - extensionPanes.forEach( - (extensionPane: ResourceTabPaneInterface) => { - panes.splice(extensionPane.index, 0, extensionPane); + if (protocol.type === "openid") { + return; } - ); - return panes; - } + const protocolName: string = mapProtocolTypeToName(protocol.type); - return [ - { - componentId: "general", - menuItem: t("applications:edit.sections.general.tabName"), - render: GeneralApplicationSettingsTabPane - }, - { - componentId: "protocol", - menuItem: t("applications:edit.sections.access.tabName"), - render: ApplicationSettingsTabPane - }, - { - componentId: "user-attributes", - menuItem: t("applications:edit.sections.attributes.tabName"), - render: AttributeSettingTabPane - }, - { - componentId: "sign-in-method", - menuItem: t("applications:edit.sections.signOnMethod.tabName"), - render: SignOnMethodsTabPane - }, - applicationConfig.editApplication.showProvisioningSettings && { - componentId: "provisioning", - menuItem: t("applications:edit.sections.provisioning.tabName"), - render: ProvisioningSettingsTabPane - }, - { - componentId: "advanced", - menuItem: t("applications:edit.sections.advanced.tabName"), - render: AdvancedSettingsTabPane - }, - { - componentId: "shared-access", - menuItem: t("applications:edit.sections.sharedAccess.tabName"), - render: SharedAccessTabPane - }, - { - componentId: "info", - menuItem: { - content: t("applications:edit.sections.info.tabName"), - icon: "info circle grey" - }, - render: InfoTabPane - } - ]; - }; - - const renderedTabPanes: ResourceTabPaneInterface[] = useMemo( - () => resolveTabPanes(), [ - tabPaneExtensions, - application?.templateId - ]); - - /** - * Set the defaultTabIndex when the application template updates. - */ - useEffect(() => { - - if(!template) { - return; - } - - let defaultTabIndex: number = 0; - - if(applicationConfig.editApplication.extendTabs) { - defaultTabIndex=1; - } - - setDefaultActiveIndex(defaultTabIndex); - - if(isEmpty(window.location.hash)){ - if(urlSearchParams.get(ApplicationManagementConstants.APP_STATE_STRONG_AUTH_PARAM_KEY) === - ApplicationManagementConstants.APP_STATE_STRONG_AUTH_PARAM_VALUE) { - // When application selection is done through the strong authentication flow. - const signInMethodtabIndex: number = renderedTabPanes?.findIndex( - (element: {"componentId": string}) => - element.componentId === ApplicationManagementConstants.SIGN_IN_METHOD_TAB_URL_FRAG - ); - - if (signInMethodtabIndex !== -1) { - handleActiveTabIndexChange(signInMethodtabIndex); - } - - return; - } else if (urlSearchParams.get(ApplicationManagementConstants.IS_PROTOCOL) === "true") { - const protocolTabIndex: number = renderedTabPanes?.findIndex( - (element: {"componentId": string}) => - element.componentId === ApplicationManagementConstants.PROTOCOL_TAB_URL_FRAG - ); - - if(protocolTabIndex !== -1) { - handleActiveTabIndexChange(protocolTabIndex); - } - - return; - } else if (urlSearchParams.get(ApplicationManagementConstants.IS_ROLES) === "true") { - const rolesTabIndex: number = renderedTabPanes?.findIndex( - (element: {"componentId": string}) => - element.componentId === ApplicationManagementConstants.ROLES_TAB_URL_FRAG - ); - - if (rolesTabIndex !== -1) { - handleActiveTabIndexChange(rolesTabIndex); + if(!protocolNames.includes(protocolName)) { + protocolNames.push(protocolName); + inboundProtocolRequests.push(getInboundProtocolConfig(appId, protocolName)); } + }); - return; - } - else { - handleDefaultTabIndexChange(defaultTabIndex); - } - } - },[ template, renderedTabPanes, urlSearchParams ]); - - /** - * Check whether the application is an M2M Application. - */ - useEffect(() => { - - if (template?.id === ApplicationTemplateIdTypes.M2M_APPLICATION) { - setM2MApplication(true); - } - }, [ template ]); - - /** - * Called when the URL fragment updates. - */ - useEffect( () => { - - if(totalTabs === undefined || window.location.hash.includes(URLFragmentTypes.VIEW) || - isEmpty(window.location.hash)) { - - return; - } - - const urlFragment: string[] = window.location.hash.split("#"+URLFragmentTypes.TAB_INDEX); - - if(urlFragment.length === 2 && isEmpty(urlFragment[0]) && /^\d+$/.test(urlFragment[1])) { - - const tabIndex: number = parseInt(urlFragment[1], 10); - - if(tabIndex === activeTabIndex) { - return; - } - - handleActiveTabIndexChange(tabIndex); - } else if (window.location.hash.includes(ApplicationManagementConstants.SIGN_IN_METHOD_TAB_URL_FRAG)) { - // Handle loading sign-in method tab when redirecting from the "Connected Apps" Tab of an IdP. - const SignInMethodtabIndex: number = renderedTabPanes?.findIndex( - (element: {"componentId": string}) => - element.componentId === ApplicationManagementConstants.SIGN_IN_METHOD_TAB_URL_FRAG); - - handleActiveTabIndexChange(SignInMethodtabIndex); - } else { - // Change the tab index to defaultActiveIndex for invalid URL fragments. - handleDefaultTabIndexChange(defaultActiveIndex); - } - }, [ window.location.hash, totalTabs ]); + setIsInboundProtocolConfigRequestLoading(true); + Axios.all(inboundProtocolRequests).then(Axios.spread((...responses: AxiosResponse[]) => { + responses.forEach((response: AxiosResponse, index: number) => { + protocolConfigs = { + ...protocolConfigs, + [ protocolNames[ index ] ]: response + }; - /** - * Fetch the allowed origins list whenever there's an update. - */ - useEffect(() => { - const allowedCORSOrigins: string[] = []; + selectedProtocolList.push(protocolNames[ index ]); + }); - if (isSuperOrganization()) { - getCORSOrigins() - .then((response: CORSOriginsListInterface[]) => { - response.map((origin: CORSOriginsListInterface) => { - allowedCORSOrigins.push(origin.url); - }); - setAllowedOrigins(allowedCORSOrigins); - }) + })) .catch((error: AxiosError) => { + if (error?.response?.status === 404) { + return; + } + if (error?.response?.data?.description) { dispatch(addAlert({ description: error.response.data.description, level: AlertLevels.ERROR, - message: t("applications:notifications.fetchAllowedCORSOrigins." + - "error.message") + message: t("applications:notifications.getInboundProtocolConfig" + + ".error.message") })); return; } dispatch(addAlert({ - description: t("applications:notifications.fetchAllowedCORSOrigins" + + description: t("applications:notifications.getInboundProtocolConfig" + ".genericError.description"), level: AlertLevels.ERROR, - message: t("applications:notifications.fetchAllowedCORSOrigins." + - "genericError.message") + message: t("applications:notifications.getInboundProtocolConfig" + + ".genericError.message") })); + }) + .finally(() => { + // Mutate the saml: NameIDFormat property according to the specification. + normalizeSAMLNameIDFormat(protocolConfigs); + setIsApplicationUpdated(true); + setInboundProtocolList(selectedProtocolList); + setInboundProtocolConfig(protocolConfigs); + setIsInboundProtocolConfigRequestLoading(false); + getConfiguredInboundProtocolsList(selectedProtocolList); + getConfiguredInboundProtocolConfigs(protocolConfigs); }); + } else { + setInboundProtocolList([]); + setInboundProtocolConfig({}); + setIsInboundProtocolConfigRequestLoading(false); + getConfiguredInboundProtocolsList([]); + getConfiguredInboundProtocolConfigs({}); } - }, [ isAllowedOriginsUpdated ]); - - /** - * Called on `availableInboundProtocols` prop update. - */ - useEffect(() => { - if (!isEmpty(availableInboundProtocols)) { - return; - } - - setInboundProtocolsRequestLoading(true); - - ApplicationManagementUtils.getInboundProtocols(InboundProtocolsMeta, false) - .finally(() => { - setInboundProtocolsRequestLoading(false); - }); - }, [ availableInboundProtocols ]); + }; /** - * Watch for `inboundProtocols` array change and fetch configured protocols if there's a difference. + * Called when an application updates. */ - useEffect(() => { - if (!application?.inboundProtocols || !application?.id) { + const handleProtocolUpdate = (): void => { + if (!application?.id) { return; } findConfiguredInboundProtocol(application.id); - }, [ JSON.stringify(application?.inboundProtocols) ]); - - useEffect(() => { - if (samlConfigurations !== undefined) { - return; - } - setSAMLConfigsLoading(true); - - ApplicationManagementUtils.getSAMLApplicationMeta() - .finally(() => { - setSAMLConfigsLoading(false); - }); - }, [ samlConfigurations, inboundProtocolConfig ]); - - useEffect(() => { - if (oidcConfigurations !== undefined) { - return; - } - setOIDCConfigsLoading(true); - - ApplicationManagementUtils.getOIDCApplicationMeta() - .finally(() => { - setOIDCConfigsLoading(false); - }); - }, [ oidcConfigurations, inboundProtocolConfig ]); - - useEffect(() => { - if (tabPaneExtensions && !isApplicationUpdated) { - return; - } - - if (!application?.id || isInboundProtocolConfigRequestLoading) { - return; - } - - if (inboundProtocolConfig && samlConfigurations && samlConfigurations.certificate) { - inboundProtocolConfig.certificate = samlConfigurations.certificate; - inboundProtocolConfig.ssoUrl = samlConfigurations.ssoUrl; - inboundProtocolConfig.issuer = samlConfigurations.issuer; - } - - const extensions: ResourceTabPaneInterface[] = applicationConfig.editApplication.getTabExtensions( - { - application: application, - content: template?.content?.quickStart, - inboundProtocolConfig: inboundProtocolConfig, - inboundProtocols: inboundProtocolList, - onApplicationUpdate: () => { - handleApplicationUpdate(application?.id); - }, - onTriggerTabUpdate: (tabIndex: number) => { - setActiveTabIndex(tabIndex); - }, - template: template - }, - featureConfig, - readOnly, - tenantDomain - ); - - setTabPaneExtensions(extensions); - setIsApplicationUpdated(false); - }, [ - tabPaneExtensions, - template, - application, - inboundProtocolList, - inboundProtocolConfig, - isInboundProtocolConfigRequestLoading - ]); - - useEffect(() => { - - if (!urlSearchParams.get(ApplicationManagementConstants.CLIENT_SECRET_HASH_ENABLED_URL_SEARCH_PARAM_KEY)) { - return; - } - - setShowClientSecretHashDisclaimerModal(true); - }, [ urlSearchParams.get(ApplicationManagementConstants.CLIENT_SECRET_HASH_ENABLED_URL_SEARCH_PARAM_KEY) ]); + }; /** - * Handles the defaultActiveIndex change. + * Handles application secret regenerate. + * @param config - Config response. */ - const handleDefaultTabIndexChange = (defaultActiveIndex: number): void => { - - if (template.id === CustomApplicationTemplate.id && defaultActiveIndex > 0) { - handleActiveTabIndexChange(defaultActiveIndex - 1); + const handleApplicationSecretRegenerate = (config: OIDCDataInterface): void => { + if (isEmpty(config) || !config.clientSecret || !config.clientId) { return; } - handleActiveTabIndexChange(defaultActiveIndex); - }; - - /** - * Handles the activeTabIndex change. - * - * @param tabIndex - Active tab index. - */ - const handleActiveTabIndexChange = (tabIndex:number): void => { - - history.push({ - hash: `#${ URLFragmentTypes.TAB_INDEX }${ tabIndex }`, - pathname: window.location.pathname + setClientSecretHashDisclaimerModalInputs({ + clientId: config.clientId, + clientSecret: config.clientSecret }); - setActiveTabIndex(tabIndex); + setShowClientSecretHashDisclaimerModal(true); }; /** - * Handles the tab change. + * Handles the toggle change to show confirmation modal. * - * @param e - Click event. - * @param data - Tab properties. + * @param event - Form event. + * @param data - Checkbox data. */ - const handleTabChange = (e: SyntheticEvent, data: TabProps): void => { - eventPublisher.compute(() => { - eventPublisher.publish("application-switch-edit-application-tabs", { - type: data.panes[data.activeIndex].componentId - }); - }); - - handleActiveTabIndexChange(data.activeIndex as number); + const handleAppEnableDisableToggleChange = (event: FormEvent, data: CheckboxProps): void => { + setEnableStatus(data?.checked); + setShowDisableConfirmationModal(true); }; /** - * Todo Remove this mapping and fix the backend. - */ - const mapProtocolTypeToName = ((type: string): string => { - let protocolName: string = type; - - if (protocolName === "oauth2") { - protocolName = SupportedAuthProtocolTypes.OIDC; - } else if (protocolName === "passivests") { - protocolName = SupportedAuthProtocolTypes.WS_FEDERATION; - } else if (protocolName === "wstrust") { - protocolName = SupportedAuthProtocolTypes.WS_TRUST; - } else if (protocolName === "samlsso") { - protocolName = SupportedAuthProtocolTypes.SAML; - } - - return protocolName; - }); - - /** - * This function will normalize the SAML name ID format - * returned by the API. - * - * @param protocolConfigs - Protocol config object - */ - const normalizeSAMLNameIDFormat = (protocolConfigs: any): void => { - const key: string = "saml"; - - if (protocolConfigs[ key ]) { - const assertion: any = protocolConfigs[ key ].singleSignOnProfile?.assertion; - - if (assertion) { - const ref: string = assertion.nameIdFormat as string; - - assertion.nameIdFormat = ref.replace(/\//g, ":"); - } - } - }; - - /** - * Finds the configured inbound protocol. - */ - const findConfiguredInboundProtocol = (appId: string): void => { - let protocolConfigs: any = {}; - const selectedProtocolList: string[] = []; - const inboundProtocolRequests: Promise[] = []; - const protocolNames: string[] = []; - - if (application?.inboundProtocols?.length > 0) { - application.inboundProtocols.forEach((protocol: InboundProtocolListItemInterface) => { - - if (protocol.type === "openid") { - return; - } - - const protocolName: string = mapProtocolTypeToName(protocol.type); - - if(!protocolNames.includes(protocolName)) { - protocolNames.push(protocolName); - inboundProtocolRequests.push(getInboundProtocolConfig(appId, protocolName)); - } - }); - - setIsInboundProtocolConfigRequestLoading(true); - Axios.all(inboundProtocolRequests).then(Axios.spread((...responses: AxiosResponse[]) => { - responses.forEach((response: AxiosResponse, index: number) => { - protocolConfigs = { - ...protocolConfigs, - [ protocolNames[ index ] ]: response - }; - - selectedProtocolList.push(protocolNames[ index ]); - }); - - })) - .catch((error: AxiosError) => { - if (error?.response?.status === 404) { - return; - } - - if (error?.response && error?.response?.data && error?.response?.data?.description) { - dispatch(addAlert({ - description: error.response?.data?.description, - level: AlertLevels.ERROR, - message: t("applications:notifications.getInboundProtocolConfig" + - ".error.message") - })); - - return; - } - - dispatch(addAlert({ - description: t("applications:notifications.getInboundProtocolConfig" + - ".genericError.description"), - level: AlertLevels.ERROR, - message: t("applications:notifications.getInboundProtocolConfig" + - ".genericError.message") - })); - }) - .finally(() => { - // Mutate the saml: NameIDFormat property according to the specification. - normalizeSAMLNameIDFormat(protocolConfigs); - setIsApplicationUpdated(true); - setInboundProtocolList(selectedProtocolList); - setInboundProtocolConfig(protocolConfigs); - setIsInboundProtocolConfigRequestLoading(false); - getConfiguredInboundProtocolsList(selectedProtocolList); - getConfiguredInboundProtocolConfigs(protocolConfigs); - }); - } else { - setInboundProtocolList([]); - setInboundProtocolConfig({}); - setIsInboundProtocolConfigRequestLoading(false); - getConfiguredInboundProtocolsList([]); - getConfiguredInboundProtocolConfigs({}); - } - }; - - /** - * Called when an application updates. - */ - const handleProtocolUpdate = (): void => { - if (!application?.id) { - return; - } - - findConfiguredInboundProtocol(application.id); - }; - - /** - * Handles application secret regenerate. - * @param config - Config response. - */ - const handleApplicationSecretRegenerate = (config: OIDCDataInterface): void => { - - if (isEmpty(config) || !config.clientSecret || !config.clientId) { - return; - } - - setClientSecretHashDisclaimerModalInputs({ - clientId: config.clientId, - clientSecret: config.clientSecret - }); - setShowClientSecretHashDisclaimerModal(true); - }; - - /** - * Handles the toggle change to show confirmation modal. - * - * @param event - Form event. - * @param data - Checkbox data. - */ - const handleAppEnableDisableToggleChange = (event: FormEvent, data: CheckboxProps): void => { - setEnableStatus(data?.checked); - setShowDisableConfirmationModal(true); - }; - - /** - * Disables an application. + * Disables an application. */ const handleApplicationDisable = (): void => { setIsDisableInProgress(true); @@ -1083,160 +531,760 @@ export const EditApplication: FunctionComponent = ); - const GeneralApplicationSettingsTabPane = (): ReactElement => ( - - - - ); + const GeneralApplicationSettingsTabPane = (): ReactElement => ( + + + + ); + + const ApplicationSettingsTabPane = (): ReactElement => ( + + setIsAllowedOriginsUpdated(!isAllowedOriginsUpdated) } + onApplicationSecretRegenerate={ handleApplicationSecretRegenerate } + appId={ application?.id } + appName={ application?.name } + applicationTemplateId={ application?.templateId } + extendedAccessConfig={ tabPaneExtensions !== undefined } + isLoading={ isLoading } + setIsLoading={ setIsLoading } + onUpdate={ handleApplicationUpdate } + onProtocolUpdate = { handleProtocolUpdate } + isInboundProtocolConfigRequestLoading={ isInboundProtocolConfigRequestLoading } + inboundProtocolsLoading={ isInboundProtocolConfigRequestLoading } + inboundProtocolConfig={ inboundProtocolConfig } + inboundProtocols={ inboundProtocolList } + featureConfig={ featureConfig } + template={ template } + readOnly={ readOnly || applicationConfig.editApplication.getTabPanelReadOnlyStatus( + "APPLICATION_EDIT_ACCESS_CONFIG", application) } + isDefaultApplication={ ApplicationManagementConstants.DEFAULT_APPS.includes(application?.name) } + isSystemApplication={ ApplicationManagementConstants.SYSTEM_APPS.includes(application?.name) } + data-componentid={ `${ componentId }-access-settings` } + /> + + ); + + const AttributeSettingTabPane = (): ReactElement => ( + + + + ); + + + const SignOnMethodsTabPane = (): ReactElement => ( + + + + + + ); + + const AdvancedSettingsTabPane = (): ReactElement => ( + + + + ); + + const ProvisioningSettingsTabPane = (): ReactElement => ( + applicationConfig.editApplication.showProvisioningSettings + ? ( + < ResourceTab.Pane controlledSegmentation> + + + ) + : null + ); + + const SharedAccessTabPane = (): ReactElement => ( + + + + ); + + const InfoTabPane = (): ReactElement => ( + + + + ); + + /** + * Renders a dynamic application edit tab pane. + * + * @param tab - The metadata for the tab. + * @returns The rendered tab pane. + */ + const DynamicApplicationEditTabPane = (tab: ApplicationEditTabMetadataInterface): ReactElement => { + const firstProtocolName: string = mapProtocolTypeToName(application?.inboundProtocols?.[0]?.type); + + return ( + + + + ); + }; + + /** + * Renders a markdown guide tab pane. + * + * @param guideContent - Content to display in Markdown format. + * @returns The rendered tab pane. + */ + const MarkdownGuideTabPane = (guideContent: string): ReactElement => { + const firstProtocolName: string = mapProtocolTypeToName(application?.inboundProtocols?.[0]?.type); + + return ( + + + + ); + }; + + /** + * Resolves the tab panes based on the application config. + * + * @returns Resolved tab panes. + */ + const resolveTabPanes = (): ResourceTabPaneInterface[] => { + const panes: ResourceTabPaneInterface[] = []; + const extensionPanes: ResourceTabPaneInterface[] = []; + + if (!tabPaneExtensions && applicationConfig.editApplication.extendTabs + && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_OIDC + && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS + && application?.templateId !== ApplicationManagementConstants.CUSTOM_APPLICATION_SAML) { + return []; + } + + if (tabPaneExtensions && tabPaneExtensions?.length > 0) { + extensionPanes.push(...cloneDeep(tabPaneExtensions)); + } + + if (featureConfig) { + if (isMyAccount) { + panes.push({ + componentId: "overview", + menuItem: t("applications:myaccount.overview.tabName"), + render: MyAccountOverviewTabPane + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_GENERAL_SETTINGS")) + && (isSubOrganization() ? + !brandingDisabledFeatures.includes( + BrandingPreferencesConstants.APP_WISE_BRANDING_FEATURE_TAG): true) + && !isMyAccount + ) { + if (applicationConfig.editApplication. + isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.GENERAL, + tenantDomain + )) { + panes.push({ + componentId: "general", + "data-tabid": ApplicationTabIDs.GENERAL, + menuItem: + + { t("applications:edit.sections.general.tabName") } + , + render: () => + applicationConfig.editApplication. + getOveriddenTab( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.GENERAL, + GeneralApplicationSettingsTabPane(), + application?.name, + application?.id, + tenantDomain + ) + }); + } + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ACCESS_CONFIG")) + && !isFragmentApp + && !isMyAccount + ) { + + applicationConfig.editApplication.isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.PROTOCOL, + tenantDomain + ) && + panes.push({ + componentId: "protocol", + "data-tabid": ApplicationTabIDs.PROTOCOL, + menuItem: t("applications:edit.sections.access.tabName"), + render: ApplicationSettingsTabPane + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ATTRIBUTE_MAPPING")) + && !isFragmentApp + && !isM2MApplication + && (UIConfig?.legacyMode?.applicationSystemAppsSettings || + application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME) + ) { + + applicationConfig.editApplication.isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.USER_ATTRIBUTES, tenantDomain) && + panes.push({ + componentId: "user-attributes", + "data-tabid": ApplicationTabIDs.USER_ATTRIBUTES, + menuItem: + + { t("applications:edit.sections.attributes.tabName") } + , + render: () => + applicationConfig.editApplication. + getOveriddenTab( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.USER_ATTRIBUTES, + AttributeSettingTabPane(), + application?.name, + application?.id, + tenantDomain + ) + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_SIGN_ON_METHOD_CONFIG")) + && !isM2MApplication) { + + applicationConfig.editApplication. + isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.SIGN_IN_METHOD, tenantDomain) && + panes.push({ + componentId: "sign-in-method", + "data-tabid": ApplicationTabIDs.SIGN_IN_METHODS, + menuItem: + + { t("applications:edit.sections.signOnMethod.tabName") } + , + render: SignOnMethodsTabPane + }); + } + if (applicationConfig.editApplication.showProvisioningSettings + && isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_PROVISIONING_SETTINGS")) + && !isFragmentApp + && !isM2MApplication + && (UIConfig?.legacyMode?.applicationSystemAppsSettings || + application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME)) { + + applicationConfig.editApplication.isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, ApplicationTabTypes.PROVISIONING, tenantDomain) && + panes.push({ + componentId: "provisioning", + "data-tabid": ApplicationTabIDs.PROVISIONING, + menuItem: t("applications:edit.sections.provisioning.tabName"), + render: ProvisioningSettingsTabPane + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_ADVANCED_SETTINGS")) + && !isFragmentApp + && !isM2MApplication + && (UIConfig?.legacyMode?.applicationSystemAppsSettings || + application?.name !== ApplicationManagementConstants.MY_ACCOUNT_APP_NAME)) { + + applicationConfig.editApplication. + isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId , ApplicationTabTypes.ADVANCED, tenantDomain) && + panes.push({ + componentId: "advanced", + "data-tabid": ApplicationTabIDs.ADVANCED, + menuItem: ( + + { t("applications:edit.sections.advanced.tabName") } + ), + render: AdvancedSettingsTabPane + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_SHARED_ACCESS")) + && application?.templateId != ApplicationManagementConstants.CUSTOM_APPLICATION_PASSIVE_STS + && !isFragmentApp + && !isM2MApplication + && applicationConfig.editApplication.showApplicationShare + && (isFirstLevelOrg || window[ "AppUtils" ].getConfig().organizationName) + && hasApplicationUpdatePermissions + && orgType !== OrganizationType.SUBORGANIZATION + && !ApplicationManagementConstants.SYSTEM_APPS.includes(application?.clientId)) { + applicationConfig.editApplication. + isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.INFO, + tenantDomain + ) && + panes.push({ + componentId: "shared-access", + "data-tabid": ApplicationTabIDs.SHARED_ACCESS, + menuItem: t("applications:edit.sections.sharedAccess.tabName"), + render: SharedAccessTabPane + }); + } + if (isFeatureEnabled(featureConfig?.applications, + ApplicationManagementConstants.FEATURE_DICTIONARY.get("APPLICATION_EDIT_INFO")) + && !isFragmentApp + && !isMyAccount) { + + applicationConfig.editApplication. + isTabEnabledForApp( + inboundProtocolConfig?.oidc?.clientId, + ApplicationTabTypes.INFO, + tenantDomain + ) && + panes.push({ + componentId: "info", + "data-tabid": ApplicationTabIDs.INFO, + menuItem: { + content: t("applications:edit.sections.info.tabName"), + icon: "info circle grey" + }, + render: InfoTabPane + }); + } + + extensionPanes.forEach( + (extensionPane: ResourceTabPaneInterface) => { + panes.splice(extensionPane?.index, 0, extensionPane); + } + ); + + return panes; + } + + return [ + { + componentId: "general", + "data-tabid": ApplicationTabIDs.GENERAL, + menuItem: t("applications:edit.sections.general.tabName"), + render: GeneralApplicationSettingsTabPane + }, + { + componentId: "protocol", + "data-tabid": ApplicationTabIDs.PROTOCOL, + menuItem: t("applications:edit.sections.access.tabName"), + render: ApplicationSettingsTabPane + }, + { + componentId: "user-attributes", + "data-tabid": ApplicationTabIDs.USER_ATTRIBUTES, + menuItem: t("applications:edit.sections.attributes.tabName"), + render: AttributeSettingTabPane + }, + { + componentId: "sign-in-method", + "data-tabid": ApplicationTabIDs.SIGN_IN_METHODS, + menuItem: t("applications:edit.sections.signOnMethod.tabName"), + render: SignOnMethodsTabPane + }, + applicationConfig.editApplication.showProvisioningSettings && { + componentId: "provisioning", + "data-tabid": ApplicationTabIDs.PROVISIONING, + menuItem: t("applications:edit.sections.provisioning.tabName"), + render: ProvisioningSettingsTabPane + }, + { + componentId: "advanced", + "data-tabid": ApplicationTabIDs.ADVANCED, + menuItem: t("applications:edit.sections.advanced.tabName"), + render: AdvancedSettingsTabPane + }, + { + componentId: "shared-access", + "data-tabid": ApplicationTabIDs.SHARED_ACCESS, + menuItem: t("applications:edit.sections.sharedAccess.tabName"), + render: SharedAccessTabPane + }, + { + componentId: "info", + "data-tabid": ApplicationTabIDs.INFO, + menuItem: { + content: t("applications:edit.sections.info.tabName"), + icon: "info circle grey" + }, + render: InfoTabPane + } + ]; + }; + + /** + * Filter the available tabs based on metadata defined in the extension template. + * + * @param tabs - All available tabs. + * @returns Filtered tabs list. + */ + const filterTabsBasedOnExtensionTemplateMetadata = ( + tabs: ResourceTabPaneInterface[] + ): ResourceTabPaneInterface[] => { + const filteredTabs: ResourceTabPaneInterface[] = []; + + /** + * Check the existence of predefined tab based on the given tab ID and + * add it to the filtered list. + * + * @param currentTab - Metadata for the tab defined in the template. + */ + const addPredefineTab = (currentTab: ApplicationEditTabMetadataInterface) => { + const predefineTab: ResourceTabPaneInterface = + tabs?.find((item: ResourceTabPaneInterface) => item?.["data-tabid"] === currentTab?.id); + + if (predefineTab) { + if (currentTab?.displayName) { + predefineTab.menuItem = currentTab?.displayName; + } + + filteredTabs.push(predefineTab); + } + }; + + extensionTemplateMetadata?.edit?.tabs?.forEach((tab: ApplicationEditTabMetadataInterface) => { + switch (tab?.contentType) { + case ApplicationEditTabContentType.GUIDE: + if (tab?.guide) { + filteredTabs.push({ + componentId: tab?.id, + "data-tabid": tab?.id, + menuItem: tab?.displayName, + render: () => MarkdownGuideTabPane(tab?.guide) + }); + } + + break; + case ApplicationEditTabContentType.FORM: + if (tab?.form) { + filteredTabs.push({ + componentId: tab?.id, + "data-tabid": tab?.id, + menuItem: tab?.displayName, + render: () => DynamicApplicationEditTabPane(tab) + }); + } + + break; + default: + addPredefineTab(tab); + } + }); + + return filteredTabs?.length > 0 ? filteredTabs : tabs; + }; + + /** + * Generate the final rendered tab list based on template metadata. + * + * @returns Tab panes list. + */ + const getFinalRenderingTabPanes = (): ResourceTabPaneInterface[] => { + const availableTabs: ResourceTabPaneInterface[] = resolveTabPanes(); + + if (extensionTemplateMetadata?.edit?.tabs + && Array.isArray(extensionTemplateMetadata?.edit?.tabs) + && extensionTemplateMetadata?.edit?.tabs?.length > 0) { + return filterTabsBasedOnExtensionTemplateMetadata(availableTabs); + } + + return availableTabs; + }; + + /** + * The list of tab panes rendered in the final render. + */ + const renderedTabPanes: ResourceTabPaneInterface[] = getFinalRenderingTabPanes(); + + /** + * Get the default active tab. + */ + const getDefaultActiveTab = (): number | string => { + + let defaultTab: number | string = 0; + + if(applicationConfig.editApplication.extendTabs && template?.id !== CustomApplicationTemplate.id) { + defaultTab = 1; + } + + if (extensionTemplateMetadata?.edit?.defaultActiveTabId) { + defaultTab = extensionTemplateMetadata?.edit?.defaultActiveTabId; + } + + return defaultTab; + }; + + /** + * When the strong authentication parameter is set to true, set the sign-in methods tab. + */ + useEffect(() => { + + if(isEmpty(window?.location?.hash)){ + if(urlSearchParams.get(ApplicationManagementConstants.APP_STATE_STRONG_AUTH_PARAM_KEY) === + ApplicationManagementConstants.APP_STATE_STRONG_AUTH_PARAM_VALUE) { + window.location.hash = TAB_URL_HASH_FRAGMENT + ApplicationTabIDs.SIGN_IN_METHODS; + } else if (urlSearchParams.get(ApplicationManagementConstants.IS_PROTOCOL) === "true") { + window.location.hash = TAB_URL_HASH_FRAGMENT + ApplicationTabIDs.PROTOCOL; + } else if (urlSearchParams.get(ApplicationManagementConstants.IS_ROLES) === "true") { + window.location.hash = TAB_URL_HASH_FRAGMENT + ApplicationTabIDs.APPLICATION_ROLES; + } + } + },[ urlSearchParams ]); + + /** + * Check whether the application is an M2M Application. + */ + useEffect(() => { + + if (template?.id === ApplicationTemplateIdTypes.M2M_APPLICATION) { + setM2MApplication(true); + } + }, [ template ]); + + /** + * Fetch the allowed origins list whenever there's an update. + */ + useEffect(() => { + const allowedCORSOrigins: string[] = []; + + if (isSuperOrganization()) { + getCORSOrigins() + .then((response: CORSOriginsListInterface[]) => { + response?.map((origin: CORSOriginsListInterface) => { + allowedCORSOrigins.push(origin?.url); + }); + setAllowedOrigins(allowedCORSOrigins); + }) + .catch((error: AxiosError) => { + if (error?.response?.data?.description) { + dispatch(addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications.fetchAllowedCORSOrigins." + + "error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applications:notifications.fetchAllowedCORSOrigins" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications.fetchAllowedCORSOrigins." + + "genericError.message") + })); + }); + } + }, [ isAllowedOriginsUpdated ]); + + /** + * Called on `availableInboundProtocols` prop update. + */ + useEffect(() => { + if (!isEmpty(availableInboundProtocols)) { + return; + } - const ApplicationSettingsTabPane = (): ReactElement => ( - - setIsAllowedOriginsUpdated(!isAllowedOriginsUpdated) } - onApplicationSecretRegenerate={ handleApplicationSecretRegenerate } - appId={ application?.id } - appName={ application?.name } - applicationTemplateId={ application?.templateId } - extendedAccessConfig={ tabPaneExtensions !== undefined } - isLoading={ isLoading } - setIsLoading={ setIsLoading } - onUpdate={ handleApplicationUpdate } - onProtocolUpdate = { handleProtocolUpdate } - isInboundProtocolConfigRequestLoading={ isInboundProtocolConfigRequestLoading } - inboundProtocolsLoading={ isInboundProtocolConfigRequestLoading } - inboundProtocolConfig={ inboundProtocolConfig } - inboundProtocols={ inboundProtocolList } - featureConfig={ featureConfig } - template={ template } - readOnly={ readOnly || applicationConfig.editApplication.getTabPanelReadOnlyStatus( - "APPLICATION_EDIT_ACCESS_CONFIG", application) } - isDefaultApplication={ ApplicationManagementConstants.DEFAULT_APPS.includes(application?.name) } - isSystemApplication={ ApplicationManagementConstants.SYSTEM_APPS.includes(application?.name) } - data-componentid={ `${ componentId }-access-settings` } - /> - - ); + setInboundProtocolsRequestLoading(true); - const AttributeSettingTabPane = (): ReactElement => ( - - - - ); + ApplicationManagementUtils.getInboundProtocols(InboundProtocolsMeta, false) + .finally(() => { + setInboundProtocolsRequestLoading(false); + }); + }, [ availableInboundProtocols ]); + /** + * Watch for `inboundProtocols` array change and fetch configured protocols if there's a difference. + */ + useEffect(() => { + if (!application?.inboundProtocols || !application?.id) { + return; + } - const SignOnMethodsTabPane = (): ReactElement => ( - - - - - - ); + findConfiguredInboundProtocol(application.id); + }, [ JSON.stringify(application?.inboundProtocols) ]); - const AdvancedSettingsTabPane = (): ReactElement => ( - - - - ); + useEffect(() => { + if (samlConfigurations !== undefined) { + return; + } + setSAMLConfigsLoading(true); - const ProvisioningSettingsTabPane = (): ReactElement => ( - applicationConfig.editApplication.showProvisioningSettings - ? ( - < ResourceTab.Pane controlledSegmentation> - - - ) - : null - ); + ApplicationManagementUtils.getSAMLApplicationMeta() + .finally(() => { + setSAMLConfigsLoading(false); + }); + }, [ samlConfigurations, inboundProtocolConfig ]); - const SharedAccessTabPane = (): ReactElement => ( - - - - ); + useEffect(() => { + if (oidcConfigurations !== undefined) { + return; + } + setOIDCConfigsLoading(true); - const InfoTabPane = (): ReactElement => ( - - - - ); + ApplicationManagementUtils.getOIDCApplicationMeta() + .finally(() => { + setOIDCConfigsLoading(false); + }); + }, [ oidcConfigurations, inboundProtocolConfig ]); + + useEffect(() => { + if (tabPaneExtensions && !isApplicationUpdated) { + return; + } + + if (!application?.id || isInboundProtocolConfigRequestLoading) { + return; + } + + if (inboundProtocolConfig && samlConfigurations && samlConfigurations?.certificate) { + inboundProtocolConfig.certificate = samlConfigurations?.certificate; + inboundProtocolConfig.ssoUrl = samlConfigurations?.ssoUrl; + inboundProtocolConfig.issuer = samlConfigurations?.issuer; + } + + const extensions: ResourceTabPaneInterface[] = applicationConfig.editApplication.getTabExtensions( + { + application: application, + content: template?.content?.quickStart, + inboundProtocolConfig: inboundProtocolConfig, + inboundProtocols: inboundProtocolList, + onApplicationUpdate: () => { + handleApplicationUpdate(application?.id); + }, + template: template + }, + featureConfig, + readOnly, + tenantDomain + ); + + setTabPaneExtensions(extensions); + setIsApplicationUpdated(false); + }, [ + tabPaneExtensions, + template, + application, + inboundProtocolList, + inboundProtocolConfig, + isInboundProtocolConfigRequestLoading + ]); + + useEffect(() => { + + if (!urlSearchParams.get(ApplicationManagementConstants.CLIENT_SECRET_HASH_ENABLED_URL_SEARCH_PARAM_KEY)) { + return; + } + + setShowClientSecretHashDisclaimerModal(true); + }, [ urlSearchParams.get(ApplicationManagementConstants.CLIENT_SECRET_HASH_ENABLED_URL_SEARCH_PARAM_KEY) ]); + + /** + * Handles the tab change. + * + * @param e - Click event. + * @param data - Tab properties. + */ + const handleTabChange = (e: SyntheticEvent, data: TabProps): void => { + eventPublisher.compute(() => { + eventPublisher.publish("application-switch-edit-application-tabs", { + type: data.panes[data.activeIndex].componentId + }); + }); + }; /** * Renders the client secret hash disclaimer modal. @@ -1358,15 +1406,12 @@ export const EditApplication: FunctionComponent = ? ( <> { - setTotalTabs(panesLength); - } } + panes={ renderedTabPanes } /> { showClientSecretHashDisclaimerModal && renderClientSecretHashDisclaimerModal() } diff --git a/features/admin.applications.v1/components/forms/general-details-form.tsx b/features/admin.applications.v1/components/forms/general-details-form.tsx index 4e7c097ec08..3f1ab3531a5 100644 --- a/features/admin.applications.v1/components/forms/general-details-form.tsx +++ b/features/admin.applications.v1/components/forms/general-details-form.tsx @@ -18,8 +18,10 @@ import Link from "@oxygen-ui/react/Link"; import { PaletteIcon } from "@oxygen-ui/react-icons"; +import { ApplicationTabComponentsFilter } from + "@wso2is/admin.application-templates.v1/components/application-tab-components-filter"; import { AppConstants, AppState, UIConfigInterface, history } from "@wso2is/admin.core.v1"; -import { applicationConfig } from "@wso2is/admin.extensions.v1"; +import { ApplicationTabIDs, applicationConfig } from "@wso2is/admin.extensions.v1"; import { OrganizationType } from "@wso2is/admin.organizations.v1/constants"; import { TestableComponentInterface } from "@wso2is/core/models"; import { URLUtils } from "@wso2is/core/utils"; @@ -37,7 +39,7 @@ import { FormValidation } from "@wso2is/validation"; import React, { FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { useSelector } from "react-redux"; -import { Divider } from "semantic-ui-react"; +import { Divider, Grid } from "semantic-ui-react"; import { useMyAccountStatus } from "../../api"; import { ApplicationManagementConstants } from "../../constants"; import { ApplicationInterface } from "../../models"; @@ -342,7 +344,7 @@ export const GeneralDetailsForm: FunctionComponent { updateConfigurations(values); } } @@ -353,219 +355,262 @@ export const GeneralDetailsForm: FunctionComponent - { getLink("develop.connections.newConnection.enterprise.saml.learnMore") === undefined - ? null - :