Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/six-books-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@wso2is/admin.core.v1": patch
"@wso2is/console": patch
---

Add Conditional Feature Preview Rendering Logic.
44 changes: 17 additions & 27 deletions features/admin.core.v1/components/header.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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";
Expand All @@ -96,6 +97,7 @@ const Header: FunctionComponent<HeaderPropsInterface> = ({
const { t } = useTranslation();

const { showPreviewFeaturesModal, setShowPreviewFeaturesModal } = useFeatureGate();
const { canUsePreviewFeatures } = usePreviewFeatures();
const { config: runtimeConfig } = useRuntimeConfig();

const profileInfo: ProfileInfoInterface = useSelector((state: AppState) => state.profile.profileInfo);
Expand Down Expand Up @@ -123,12 +125,6 @@ const Header: FunctionComponent<HeaderPropsInterface> = ({
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;
});
Expand Down Expand Up @@ -682,22 +678,14 @@ const Header: FunctionComponent<HeaderPropsInterface> = ({
</MenuItem>
</Show>
),
<Show key="feature.preview" featureId={ FeatureGateConstants.SAAS_FEATURES_IDENTIFIER }>
<Show
when={ [
...(loginAndRegistrationFeatureConfig?.scopes?.update ?? []),
...(cdsFeatureConfig?.scopes?.update ?? [])
] }
featureId={ FeatureGateConstants.PREVIEW_FEATURES_IDENTIFIER }
>
<MenuItem onClick={ () => setShowPreviewFeaturesModal(true) }>
<ListItemIcon>
<PreviewFeaturesIcon />
</ListItemIcon>
<ListItemText>{ t("Feature Preview") }</ListItemText>
</MenuItem>
</Show>
</Show>,
canUsePreviewFeatures && (
<MenuItem onClick={ () => setShowPreviewFeaturesModal(true) }>
<ListItemIcon>
<PreviewFeaturesIcon />
</ListItemIcon>
<ListItemText>{ t("Feature Preview") }</ListItemText>
</MenuItem>
),
isShowAppSwitchButton() ? (
<MenuItem
color="inherit"
Expand Down Expand Up @@ -726,10 +714,12 @@ const Header: FunctionComponent<HeaderPropsInterface> = ({
} }
{ ...rest }
/>
<FeaturePreviewModal
open={ showPreviewFeaturesModal }
onClose={ () => setShowPreviewFeaturesModal(false) }
/>
{ canUsePreviewFeatures && (
<FeaturePreviewModal
open={ showPreviewFeaturesModal }
onClose={ () => setShowPreviewFeaturesModal(false) }
/>
) }
</>
);
};
Expand Down
202 changes: 12 additions & 190 deletions features/admin.core.v1/components/modals/feature-preview-modal.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -123,129 +55,19 @@ const FeaturePreviewModal: FunctionComponent<FeaturePreviewModalPropsInterface>
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<HTMLInputElement>, 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<void> => {
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 (
<Dialog
onClose={ handleClose }
Expand Down
Loading
Loading