diff --git a/.changeset/six-books-clap.md b/.changeset/six-books-clap.md new file mode 100644 index 00000000000..2ecc21d79bc --- /dev/null +++ b/.changeset/six-books-clap.md @@ -0,0 +1,6 @@ +--- +"@wso2is/admin.core.v1": patch +"@wso2is/console": patch +--- + +Add Conditional Feature Preview Rendering Logic. diff --git a/features/admin.core.v1/components/header.tsx b/features/admin.core.v1/components/header.tsx index dc33832fbae..72f07bc263c 100644 --- a/features/admin.core.v1/components/header.tsx +++ b/features/admin.core.v1/components/header.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023-2025, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2023-2026, 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 @@ -71,6 +71,7 @@ import { AppConstants } from "../constants/app-constants"; import { OrganizationType } from "../constants/organization-constants"; import { history } from "../helpers/history"; import useGlobalVariables from "../hooks/use-global-variables"; +import { usePreviewFeatures } from "../hooks/use-preview-features"; import { ConfigReducerStateInterface } from "../models/reducer-state"; import { AppState, store } from "../store"; import { CommonUtils } from "../utils/common-utils"; @@ -96,6 +97,7 @@ const Header: FunctionComponent = ({ const { t } = useTranslation(); const { showPreviewFeaturesModal, setShowPreviewFeaturesModal } = useFeatureGate(); + const { canUsePreviewFeatures } = usePreviewFeatures(); const { config: runtimeConfig } = useRuntimeConfig(); const profileInfo: ProfileInfoInterface = useSelector((state: AppState) => state.profile.profileInfo); @@ -123,12 +125,6 @@ const Header: FunctionComponent = ({ const supportedI18nLanguages: SupportedLanguagesMeta = useSelector( (state: AppState) => state.global.supportedI18nLanguages ); - const loginAndRegistrationFeatureConfig: FeatureAccessConfigInterface = - useSelector((state: AppState) => state?.config?.ui?.features?.loginAndRegistration); - - const cdsFeatureConfig: FeatureAccessConfigInterface = - useSelector((state: AppState) => state?.config?.ui?.features?.customerDataService); - const isCentralDeploymentEnabled: boolean = useSelector((state: AppState) => { return state?.config?.deployment?.centralDeploymentEnabled; }); @@ -682,22 +678,14 @@ const Header: FunctionComponent = ({ ), - - - setShowPreviewFeaturesModal(true) }> - - - - { t("Feature Preview") } - - - , + canUsePreviewFeatures && ( + setShowPreviewFeaturesModal(true) }> + + + + { t("Feature Preview") } + + ), isShowAppSwitchButton() ? ( = ({ } } { ...rest } /> - setShowPreviewFeaturesModal(false) } - /> + { canUsePreviewFeatures && ( + setShowPreviewFeaturesModal(false) } + /> + ) } ); }; diff --git a/features/admin.core.v1/components/modals/feature-preview-modal.tsx b/features/admin.core.v1/components/modals/feature-preview-modal.tsx index 82d1b2207a5..84ce264332a 100644 --- a/features/admin.core.v1/components/modals/feature-preview-modal.tsx +++ b/features/admin.core.v1/components/modals/feature-preview-modal.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2025-2026, 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 @@ -34,84 +34,16 @@ import ListItemText from "@oxygen-ui/react/ListItemText"; import Stack from "@oxygen-ui/react/Stack"; import Switch from "@oxygen-ui/react/Switch"; import Typography from "@oxygen-ui/react/Typography"; -import { useRequiredScopes } from "@wso2is/access-control"; -import { updateCDSConfig } from "@wso2is/admin.cds.v1/api/config"; -import useCDSConfig from "@wso2is/admin.cds.v1/hooks/use-config"; -import useFeatureGate from "@wso2is/admin.feature-gate.v1/hooks/use-feature-gate"; -import { AlertLevels, FeatureAccessConfigInterface, IdentifiableComponentInterface } from "@wso2is/core/models"; -import { addAlert } from "@wso2is/core/store"; -import React, { ChangeEvent, FunctionComponent, ReactElement, useEffect, useMemo, useState } from "react"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import React, { ChangeEvent, FunctionComponent, ReactElement } from "react"; import { useTranslation } from "react-i18next"; -import { useDispatch, useSelector } from "react-redux"; -import NewCDSFeatureImage from "../../assets/illustrations/preview-features/new-cds-feature.png"; -import { AppConstants } from "../../constants/app-constants"; -import { history } from "../../helpers/history"; -import { AppState } from "../../store"; -import "./feature-preview-modal.scss"; - -/** Added or removed as a system application when CDS is toggled. */ -const CDS_CONSOLE_APP:string = "CONSOLE"; +import { PreviewFeaturesListInterface, usePreviewFeatures } from "../../hooks/use-preview-features"; interface FeaturePreviewModalPropsInterface extends IdentifiableComponentInterface { open: boolean; onClose: () => void; } -/** - * Preview features list interface. - */ -interface PreviewFeaturesListInterface { - /** - * Feature action. - */ - action?: string; - - /** - * Feature name. - */ - name: string; - - /** - * React component to be rendered - */ - component?: ReactElement; - - /** - * Feature description. - */ - description: string; - - /** - * Feature id. - */ - id: string; - - /** - * Feature image. - */ - image?: string; - - /** - * Whether the feature is enabled - */ - enabled?: boolean; - - /** - * Feature value. - */ - value: string; - - /** - * Required scopes to access the feature. If not provided, the feature will be accessible to all users. - */ - requiredScopes?: string[]; - - message?: { - type: "info" | "warning" | "error"; - content: string; - }; -} - /** * Feature preview modal component. * @@ -123,129 +55,19 @@ const FeaturePreviewModal: FunctionComponent onClose, open }: FeaturePreviewModalPropsInterface): ReactElement => { - const { t } = useTranslation(); - const dispatch: any = useDispatch(); - const { selectedPreviewFeatureToShow } = useFeatureGate(); - - const cdsFeatureConfig: FeatureAccessConfigInterface = useSelector( - (state: AppState) => state?.config?.ui?.features?.customerDataService - ); - const { - data: cdsConfig, - mutate: mutateCDSConfig - } = useCDSConfig(open && (cdsFeatureConfig?.enabled ?? false)); - - const hasCDSScopes: boolean = useRequiredScopes( - cdsFeatureConfig?.scopes?.update - ); - - - const previewFeaturesList: PreviewFeaturesListInterface[] = useMemo(() => ([ - { - action: t("customerDataService:common.featurePreview.action"), - description: t("customerDataService:common.featurePreview.description"), - enabled: cdsConfig?.cds_enabled, - id: "customer-data-service", - image: NewCDSFeatureImage, - message: { - content: t("customerDataService:common.featurePreview.message"), - type: "warning" as const - }, - name: t("customerDataService:common.featurePreview.name"), - requiredScopes: cdsFeatureConfig?.scopes?.update, - value: "CDS.Enable" - } - ].filter(Boolean)), [ cdsConfig, cdsFeatureConfig, t ]); - - const accessibleFeatures: PreviewFeaturesListInterface[] = useMemo(() => ( - previewFeaturesList.filter((feature: PreviewFeaturesListInterface) => { - if (feature.id === "customer-data-service") { - return hasCDSScopes && !!cdsFeatureConfig?.enabled; - } - - return true; - }) - ), [ previewFeaturesList, hasCDSScopes, cdsFeatureConfig?.enabled ]); - - const [ selectedFeatureIndex, setSelectedFeatureIndex ] = useState(0); - - const selected: PreviewFeaturesListInterface = useMemo( - () => accessibleFeatures[selectedFeatureIndex], - [ selectedFeatureIndex, accessibleFeatures ] - ); - - useEffect(() => { - const activePreviewFeatureIndex: number = accessibleFeatures.findIndex( - (feature: PreviewFeaturesListInterface) => feature?.id === selectedPreviewFeatureToShow - ); - - setSelectedFeatureIndex(activePreviewFeatureIndex > 0 ? activePreviewFeatureIndex : 0); - }, [ selectedPreviewFeatureToShow ]); - - const handleClose = () => { + accessibleFeatures, + handlePageRedirection, + handleToggleChange, + selected, + setSelectedFeatureIndex + } = usePreviewFeatures(); + + const handleClose: () => void = (): void => { onClose(); }; - const handlePageRedirection = (actionId: string) => { - switch (actionId) { - case "customer-data-service": - return history.push(AppConstants.getPaths().get("PROFILES")); - default: - return; - } - }; - - const handleToggleChange = async (e: ChangeEvent, actionId: string) => { - const isChecked: boolean = e.target.checked; - - switch (actionId) { - case "customer-data-service": - await handleCDSToggle(isChecked); - - break; - - default: - break; - } - }; - - /** - * Handles CDS enable/disable via PATCH. - * - * Enabling → set cds_enabled: true; if system_applications is empty, seed it with ["CONSOLE"]. - * Disabling → set cds_enabled: false; remove "CONSOLE" from system_applications (leave others intact). - */ - const handleCDSToggle = async (enable: boolean): Promise => { - const currentApps: string[] = cdsConfig?.system_applications ?? []; - - let nextApps: string[]; - - if (enable) { - nextApps = currentApps.length === 0 - ? [ CDS_CONSOLE_APP ] - : currentApps; - } else { - nextApps = currentApps.filter((app: string) => app !== CDS_CONSOLE_APP); - } - - try { - await updateCDSConfig({ - cds_enabled: enable, - system_applications: nextApps - }); - - mutateCDSConfig(); - } catch (error) { - dispatch(addAlert({ - description: t("customerDataService:common.featurePreview.updateError"), - level: AlertLevels.ERROR, - message: t("common:error") - })); - } - }; - return ( Promise; + handlePageRedirection: (actionId: string) => void; + handleToggleChange: (e: ChangeEvent, actionId: string) => Promise; + previewFeaturesList: PreviewFeaturesListInterface[]; + selected: PreviewFeaturesListInterface | undefined; + selectedFeatureIndex: number; + setSelectedFeatureIndex: (index: number) => void; +} + +/** + * Builds the preview features list, accessible features, and modal state/handlers. + * Used by FeaturePreviewModal for rendering and by Header to gate the Feature Preview menu item. + * + * @returns Preview features list, accessible list, hasPreviewFeatures flag, and modal state/handlers. + */ +export const usePreviewFeatures = (): UsePreviewFeaturesReturnInterface => { + const { t } = useTranslation(); + const dispatch: Dispatch = useDispatch(); + const { selectedPreviewFeatureToShow } = useFeatureGate(); + + const cdsFeatureConfig: FeatureAccessConfigInterface = useSelector( + (state: AppState) => state?.config?.ui?.features?.customerDataService + ); + + const saasFeatureStatus: FeatureStatus = useCheckFeatureStatus(FeatureGateConstants.SAAS_FEATURES_IDENTIFIER); + const previewFeaturesFeatureStatus: FeatureStatus = useCheckFeatureStatus( + FeatureGateConstants.PREVIEW_FEATURES_IDENTIFIER + ); + + const hasCDSScopes: boolean = useRequiredScopes(cdsFeatureConfig?.scopes?.update); + + const { + data: cdsConfig, + mutate: mutateCDSConfig + } = useCDSConfig(cdsFeatureConfig?.enabled ?? false); + + const previewFeaturesList: PreviewFeaturesListInterface[] = useMemo(() => { + const items: PreviewFeaturesListInterface[] = []; + + if (cdsFeatureConfig?.enabled && hasCDSScopes) { + items.push({ + action: t("customerDataService:common.featurePreview.action"), + description: t("customerDataService:common.featurePreview.description"), + enabled: cdsConfig?.cds_enabled, + id: "customer-data-service", + image: NewCDSFeatureImage, + message: { + content: t("customerDataService:common.featurePreview.message"), + type: "warning" as const + }, + name: t("customerDataService:common.featurePreview.name"), + requiredScopes: cdsFeatureConfig?.scopes?.update, + value: "CDS.Enable" + }); + } + + return items; + }, [ cdsConfig, cdsFeatureConfig, hasCDSScopes, t ]); + + const accessibleFeatures: PreviewFeaturesListInterface[] = useMemo( + () => + previewFeaturesList.filter((feature: PreviewFeaturesListInterface) => { + if (feature.id === "customer-data-service") { + return hasCDSScopes; + } + + return true; + }), + [ previewFeaturesList, hasCDSScopes ] + ); + + const [ selectedFeatureIndex, setSelectedFeatureIndex ] = useState(0); + + const selected: PreviewFeaturesListInterface | undefined = useMemo( + () => accessibleFeatures[selectedFeatureIndex], + [ selectedFeatureIndex, accessibleFeatures ] + ); + + useEffect(() => { + const activePreviewFeatureIndex: number = accessibleFeatures.findIndex( + (feature: PreviewFeaturesListInterface) => feature?.id === selectedPreviewFeatureToShow + ); + + setSelectedFeatureIndex(activePreviewFeatureIndex >= 0 ? activePreviewFeatureIndex : 0); + }, [ accessibleFeatures, selectedPreviewFeatureToShow ]); + + const handlePageRedirection: (actionId: string) => void = useCallback((actionId: string) => { + switch (actionId) { + case "customer-data-service": + history.push(AppConstants.getPaths().get("PROFILES")); + + break; + default: + break; + } + }, []); + + const handleCDSToggle: (enable: boolean) => Promise = useCallback( + async (enable: boolean): Promise => { + const currentApps: string[] = cdsConfig?.system_applications ?? []; + const nextApps: string[] = enable + ? currentApps.length === 0 + ? [ CDS_CONSOLE_APP ] + : currentApps + : currentApps.filter((app: string) => app !== CDS_CONSOLE_APP); + + try { + await updateCDSConfig({ + cds_enabled: enable, + system_applications: nextApps + }); + mutateCDSConfig(); + } catch { + dispatch( + addAlert({ + description: t("customerDataService:common.featurePreview.updateError"), + level: AlertLevels.ERROR, + message: t("common:error") + }) + ); + } + }, + [ cdsConfig?.system_applications, dispatch, mutateCDSConfig, t ] + ); + + const handleToggleChange: ( + e: ChangeEvent, + actionId: string + ) => Promise = useCallback( + async (e: ChangeEvent, actionId: string): Promise => { + const isChecked: boolean = e.target.checked; + + switch (actionId) { + case "customer-data-service": + await handleCDSToggle(isChecked); + + break; + default: + break; + } + }, + [ handleCDSToggle ] + ); + + const hasAccessiblePreviewFeatures: boolean = accessibleFeatures.length > 0; + const canUsePreviewFeatures: boolean = + saasFeatureStatus === FeatureStatus.ENABLED && + previewFeaturesFeatureStatus === FeatureStatus.ENABLED && + hasAccessiblePreviewFeatures; + + return { + accessibleFeatures, + canUsePreviewFeatures, + handleCDSToggle, + handlePageRedirection, + handleToggleChange, + previewFeaturesList, + selected, + selectedFeatureIndex, + setSelectedFeatureIndex + }; +}; +