diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 92fc8b3021..417ae6302d 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -95,7 +95,7 @@ public class Profile { LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED); private final Type type; - private String label; + private final String label; private Set dependencies; Feature(String label, Type type) { @@ -133,7 +133,7 @@ public class Profile { EXPERIMENTAL("Experimental"), DEPRECATED("Deprecated"); - private String label; + private final String label; Type(String label) { this.label = label; @@ -248,7 +248,7 @@ public class Profile { case DEFAULT: return true; case PREVIEW: - return profile.equals(ProfileName.PREVIEW) ? true : false; + return profile.equals(ProfileName.PREVIEW); default: return false; } @@ -268,12 +268,12 @@ public class Profile { } private void logUnsupportedFeatures() { - logUnsuportedFeatures(Feature.Type.PREVIEW, getPreviewFeatures(), Logger.Level.INFO); - logUnsuportedFeatures(Feature.Type.EXPERIMENTAL, getExperimentalFeatures(), Logger.Level.WARN); - logUnsuportedFeatures(Feature.Type.DEPRECATED, getDeprecatedFeatures(), Logger.Level.WARN); + logUnsupportedFeatures(Feature.Type.PREVIEW, getPreviewFeatures(), Logger.Level.INFO); + logUnsupportedFeatures(Feature.Type.EXPERIMENTAL, getExperimentalFeatures(), Logger.Level.WARN); + logUnsupportedFeatures(Feature.Type.DEPRECATED, getDeprecatedFeatures(), Logger.Level.WARN); } - private void logUnsuportedFeatures(Feature.Type type, Set checkedFeatures, Logger.Level level) { + private void logUnsupportedFeatures(Feature.Type type, Set checkedFeatures, Logger.Level level) { Set checkedFeatureTypes = checkedFeatures.stream() .map(Feature::getType) .collect(Collectors.toSet()); diff --git a/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java b/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java new file mode 100644 index 0000000000..2cafa4bc31 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/info/FeatureRepresentation.java @@ -0,0 +1,87 @@ +package org.keycloak.representations.info; + +import org.keycloak.common.Profile; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class FeatureRepresentation { + private String name; + private String label; + private Type type; + private boolean isEnabled; + private Set dependencies; + + public FeatureRepresentation() { + } + + public FeatureRepresentation(Profile.Feature feature, boolean isEnabled) { + this.name = feature.name(); + this.label = feature.getLabel(); + this.type = Type.valueOf(feature.getType().name()); + this.isEnabled = isEnabled; + this.dependencies = feature.getDependencies() != null ? + feature.getDependencies().stream().map(Enum::name).collect(Collectors.toSet()) : Collections.emptySet(); + } + + public static List create() { + List featureRepresentationList = new ArrayList<>(); + Profile profile = Profile.getInstance(); + final Map features = profile.getFeatures(); + features.forEach((f, enabled) -> featureRepresentationList.add(new FeatureRepresentation(f, enabled))); + return featureRepresentationList; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public Type getType() { + return type; + } + + public void setType(Type type) { + this.type = type; + } + + public boolean isEnabled() { + return isEnabled; + } + + public void setEnabled(boolean enabled) { + isEnabled = enabled; + } + + public Set getDependencies() { + return dependencies; + } + + public void setDependencies(Set dependencies) { + this.dependencies = dependencies; + } +} + +enum Type { + DEFAULT, + DISABLED_BY_DEFAULT, + PREVIEW, + PREVIEW_DISABLED_BY_DEFAULT, + EXPERIMENTAL, + DEPRECATED; +} \ No newline at end of file diff --git a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java index 42c479a086..adb152098d 100755 --- a/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ServerInfoRepresentation.java @@ -34,6 +34,8 @@ public class ServerInfoRepresentation { private MemoryInfoRepresentation memoryInfo; private ProfileInfoRepresentation profileInfo; + private List features; + private CryptoInfoRepresentation cryptoInfo; private Map> themes; @@ -77,6 +79,14 @@ public class ServerInfoRepresentation { this.profileInfo = profileInfo; } + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + public CryptoInfoRepresentation getCryptoInfo() { return cryptoInfo; } diff --git a/js/apps/admin-ui/src/dashboard/Dashboard.tsx b/js/apps/admin-ui/src/dashboard/Dashboard.tsx index 8281093b7c..a483d83bf8 100644 --- a/js/apps/admin-ui/src/dashboard/Dashboard.tsx +++ b/js/apps/admin-ui/src/dashboard/Dashboard.tsx @@ -1,6 +1,5 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { union, filter } from "lodash-es"; import { Brand, Card, @@ -26,13 +25,16 @@ import { Title, } from "@patternfly/react-core"; +import FeatureRepresentation, { + FeatureType, +} from "@keycloak/keycloak-admin-client/lib/defs/featureRepresentation"; import { useRealm } from "../context/realm-context/RealmContext"; import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { toUpperCase } from "../util"; import { HelpItem } from "ui-shared"; import environment from "../environment"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; -import useLocaleSort from "../utils/useLocaleSort"; +import useLocaleSort, { mapByKey } from "../utils/useLocaleSort"; import { RoutableTabs, useRoutableTab, @@ -67,48 +69,47 @@ const EmptyDashboard = () => { ); }; +type FeatureItemProps = { + feature: FeatureRepresentation; +}; + +const FeatureItem = ({ feature }: FeatureItemProps) => { + const { t } = useTranslation(); + return ( + + {feature.name}  + {feature.type === FeatureType.Experimental && ( + + )} + {feature.type === FeatureType.Preview && ( + + )} + {feature.type === FeatureType.Default && ( + + )} + + ); +}; + const Dashboard = () => { const { t } = useTranslation(); const { realm } = useRealm(); const serverInfo = useServerInfo(); const localeSort = useLocaleSort(); - const isDeprecatedFeature = (feature: string) => - disabledFeatures.includes(feature); - - const isExperimentalFeature = (feature: string) => - serverInfo.profileInfo?.experimentalFeatures?.includes(feature); - - const isPreviewFeature = (feature: string) => - serverInfo.profileInfo?.previewFeatures?.includes(feature); - - const isSupportedFeature = (feature: string) => - !isExperimentalFeature(feature) && !isPreviewFeature(feature); + const sortedFeatures = useMemo( + () => localeSort(serverInfo.features ?? [], mapByKey("name")), + [serverInfo.features], + ); const disabledFeatures = useMemo( - () => - localeSort( - serverInfo.profileInfo?.disabledFeatures ?? [], - (item) => item, - ), - [serverInfo.profileInfo], + () => sortedFeatures.filter((f) => !f.enabled) || [], + [serverInfo.features], ); const enabledFeatures = useMemo( - () => - localeSort( - filter( - union( - serverInfo.profileInfo?.experimentalFeatures, - serverInfo.profileInfo?.previewFeatures, - ), - (feature) => { - return !isDeprecatedFeature(feature); - }, - ), - (item) => item, - ), - [serverInfo.profileInfo], + () => sortedFeatures.filter((f) => f.enabled) || [], + [serverInfo.features], ); const useTab = (tab: DashboardTab) => @@ -215,17 +216,10 @@ const Dashboard = () => { {enabledFeatures.map((feature) => ( - - {feature}{" "} - {isExperimentalFeature(feature) ? ( - - ) : null} - {isPreviewFeature(feature) ? ( - - ) : null} - + ))} @@ -241,22 +235,10 @@ const Dashboard = () => { {disabledFeatures.map((feature) => ( - - {feature}{" "} - {isExperimentalFeature(feature) ? ( - - ) : null} - {isPreviewFeature(feature) ? ( - - ) : null} - {isSupportedFeature(feature) ? ( - - ) : null} - + ))} diff --git a/js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts b/js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts index bba5dbaec3..cadb040753 100644 --- a/js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts +++ b/js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts @@ -10,10 +10,15 @@ export enum Feature { } export default function useIsFeatureEnabled() { - const { profileInfo } = useServerInfo(); - const disabledFilters = profileInfo?.disabledFeatures ?? []; + const { features } = useServerInfo(); return function isFeatureEnabled(feature: Feature) { - return !disabledFilters.includes(feature); + if (!features) { + return false; + } + return features + .filter((f) => f.enabled) + .map((f) => f.name) + .includes(feature); }; } diff --git a/js/libs/keycloak-admin-client/src/defs/featureRepresentation.ts b/js/libs/keycloak-admin-client/src/defs/featureRepresentation.ts new file mode 100644 index 0000000000..b618d86b61 --- /dev/null +++ b/js/libs/keycloak-admin-client/src/defs/featureRepresentation.ts @@ -0,0 +1,16 @@ +export default interface FeatureRepresentation { + name: string; + label: string; + type: FeatureType; + enabled: boolean; + dependencies: string[]; +} + +export enum FeatureType { + Default = "DEFAULT", + DisabledByDefault = "DISABLED_BY_DEFAULT", + Preview = "PREVIEW", + PreviewDisabledByDefault = "PREVIEW_DISABLED_BY_DEFAULT", + Experimental = "EXPERIMENTAL", + Deprecated = "DEPRECATED", +} diff --git a/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts b/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts index ca49c6cabf..5cfed263c0 100644 --- a/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts +++ b/js/libs/keycloak-admin-client/src/defs/serverInfoRepesentation.ts @@ -1,5 +1,6 @@ import type ComponentTypeRepresentation from "./componentTypeRepresentation.js"; import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js"; +import FeatureRepresentation from "./featureRepresentation.js"; import type PasswordPolicyTypeRepresentation from "./passwordPolicyTypeRepresentation.js"; import type ProfileInfoRepresentation from "./profileInfoRepresentation.js"; import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js"; @@ -12,6 +13,7 @@ export interface ServerInfoRepresentation { systemInfo?: SystemInfoRepresentation; memoryInfo?: MemoryInfoRepresentation; profileInfo?: ProfileInfoRepresentation; + features?: FeatureRepresentation[]; cryptoInfo?: CryptoInfoRepresentation; themes?: { [index: string]: ThemeInfoRepresentation[] }; socialProviders?: { [index: string]: string }[]; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java index 9278e88617..9b66c7f8be 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/info/ServerInfoAdminResource.java @@ -49,6 +49,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.representations.info.ClientInstallationRepresentation; import org.keycloak.representations.info.CryptoInfoRepresentation; +import org.keycloak.representations.info.FeatureRepresentation; import org.keycloak.representations.info.MemoryInfoRepresentation; import org.keycloak.representations.info.ProfileInfoRepresentation; import org.keycloak.representations.info.ProviderRepresentation; @@ -104,6 +105,7 @@ public class ServerInfoAdminResource { info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp())); info.setMemoryInfo(MemoryInfoRepresentation.create()); info.setProfileInfo(ProfileInfoRepresentation.create()); + info.setFeatures(FeatureRepresentation.create()); // True - asymmetric algorithms, false - symmetric algorithms Map> algorithms = session.getAllProviders(ClientSignatureVerifierProvider.class).stream()