better features overview (#22641)

Closes #17733
This commit is contained in:
Erik Jan de Wit 2023-09-12 16:03:13 +02:00 committed by GitHub
parent ebc9faea79
commit 0789d3c1cc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 70 deletions

View file

@ -95,7 +95,7 @@ public class Profile {
LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED); LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED);
private final Type type; private final Type type;
private String label; private final String label;
private Set<Feature> dependencies; private Set<Feature> dependencies;
Feature(String label, Type type) { Feature(String label, Type type) {
@ -133,7 +133,7 @@ public class Profile {
EXPERIMENTAL("Experimental"), EXPERIMENTAL("Experimental"),
DEPRECATED("Deprecated"); DEPRECATED("Deprecated");
private String label; private final String label;
Type(String label) { Type(String label) {
this.label = label; this.label = label;
@ -248,7 +248,7 @@ public class Profile {
case DEFAULT: case DEFAULT:
return true; return true;
case PREVIEW: case PREVIEW:
return profile.equals(ProfileName.PREVIEW) ? true : false; return profile.equals(ProfileName.PREVIEW);
default: default:
return false; return false;
} }
@ -268,12 +268,12 @@ public class Profile {
} }
private void logUnsupportedFeatures() { private void logUnsupportedFeatures() {
logUnsuportedFeatures(Feature.Type.PREVIEW, getPreviewFeatures(), Logger.Level.INFO); logUnsupportedFeatures(Feature.Type.PREVIEW, getPreviewFeatures(), Logger.Level.INFO);
logUnsuportedFeatures(Feature.Type.EXPERIMENTAL, getExperimentalFeatures(), Logger.Level.WARN); logUnsupportedFeatures(Feature.Type.EXPERIMENTAL, getExperimentalFeatures(), Logger.Level.WARN);
logUnsuportedFeatures(Feature.Type.DEPRECATED, getDeprecatedFeatures(), Logger.Level.WARN); logUnsupportedFeatures(Feature.Type.DEPRECATED, getDeprecatedFeatures(), Logger.Level.WARN);
} }
private void logUnsuportedFeatures(Feature.Type type, Set<Feature> checkedFeatures, Logger.Level level) { private void logUnsupportedFeatures(Feature.Type type, Set<Feature> checkedFeatures, Logger.Level level) {
Set<Feature.Type> checkedFeatureTypes = checkedFeatures.stream() Set<Feature.Type> checkedFeatureTypes = checkedFeatures.stream()
.map(Feature::getType) .map(Feature::getType)
.collect(Collectors.toSet()); .collect(Collectors.toSet());

View file

@ -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<String> 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<FeatureRepresentation> create() {
List<FeatureRepresentation> featureRepresentationList = new ArrayList<>();
Profile profile = Profile.getInstance();
final Map<Profile.Feature, Boolean> 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<String> getDependencies() {
return dependencies;
}
public void setDependencies(Set<String> dependencies) {
this.dependencies = dependencies;
}
}
enum Type {
DEFAULT,
DISABLED_BY_DEFAULT,
PREVIEW,
PREVIEW_DISABLED_BY_DEFAULT,
EXPERIMENTAL,
DEPRECATED;
}

View file

@ -34,6 +34,8 @@ public class ServerInfoRepresentation {
private MemoryInfoRepresentation memoryInfo; private MemoryInfoRepresentation memoryInfo;
private ProfileInfoRepresentation profileInfo; private ProfileInfoRepresentation profileInfo;
private List<FeatureRepresentation> features;
private CryptoInfoRepresentation cryptoInfo; private CryptoInfoRepresentation cryptoInfo;
private Map<String, List<ThemeInfoRepresentation>> themes; private Map<String, List<ThemeInfoRepresentation>> themes;
@ -77,6 +79,14 @@ public class ServerInfoRepresentation {
this.profileInfo = profileInfo; this.profileInfo = profileInfo;
} }
public List<FeatureRepresentation> getFeatures() {
return features;
}
public void setFeatures(List<FeatureRepresentation> features) {
this.features = features;
}
public CryptoInfoRepresentation getCryptoInfo() { public CryptoInfoRepresentation getCryptoInfo() {
return cryptoInfo; return cryptoInfo;
} }

View file

@ -1,6 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { union, filter } from "lodash-es";
import { import {
Brand, Brand,
Card, Card,
@ -26,13 +25,16 @@ import {
Title, Title,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import FeatureRepresentation, {
FeatureType,
} from "@keycloak/keycloak-admin-client/lib/defs/featureRepresentation";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { toUpperCase } from "../util"; import { toUpperCase } from "../util";
import { HelpItem } from "ui-shared"; import { HelpItem } from "ui-shared";
import environment from "../environment"; import environment from "../environment";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import useLocaleSort from "../utils/useLocaleSort"; import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
import { import {
RoutableTabs, RoutableTabs,
useRoutableTab, useRoutableTab,
@ -67,48 +69,47 @@ const EmptyDashboard = () => {
); );
}; };
type FeatureItemProps = {
feature: FeatureRepresentation;
};
const FeatureItem = ({ feature }: FeatureItemProps) => {
const { t } = useTranslation();
return (
<ListItem className="pf-u-mb-sm">
{feature.name}&nbsp;
{feature.type === FeatureType.Experimental && (
<Label color="orange">{t("experimental")}</Label>
)}
{feature.type === FeatureType.Preview && (
<Label color="blue">{t("preview")}</Label>
)}
{feature.type === FeatureType.Default && (
<Label color="green">{t("supported")}</Label>
)}
</ListItem>
);
};
const Dashboard = () => { const Dashboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { realm } = useRealm(); const { realm } = useRealm();
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const localeSort = useLocaleSort(); const localeSort = useLocaleSort();
const isDeprecatedFeature = (feature: string) => const sortedFeatures = useMemo(
disabledFeatures.includes(feature); () => localeSort(serverInfo.features ?? [], mapByKey("name")),
[serverInfo.features],
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 disabledFeatures = useMemo( const disabledFeatures = useMemo(
() => () => sortedFeatures.filter((f) => !f.enabled) || [],
localeSort( [serverInfo.features],
serverInfo.profileInfo?.disabledFeatures ?? [],
(item) => item,
),
[serverInfo.profileInfo],
); );
const enabledFeatures = useMemo( const enabledFeatures = useMemo(
() => () => sortedFeatures.filter((f) => f.enabled) || [],
localeSort( [serverInfo.features],
filter(
union(
serverInfo.profileInfo?.experimentalFeatures,
serverInfo.profileInfo?.previewFeatures,
),
(feature) => {
return !isDeprecatedFeature(feature);
},
),
(item) => item,
),
[serverInfo.profileInfo],
); );
const useTab = (tab: DashboardTab) => const useTab = (tab: DashboardTab) =>
@ -215,17 +216,10 @@ const Dashboard = () => {
<DescriptionListDescription> <DescriptionListDescription>
<List variant={ListVariant.inline}> <List variant={ListVariant.inline}>
{enabledFeatures.map((feature) => ( {enabledFeatures.map((feature) => (
<ListItem key={feature} className="pf-u-mb-sm"> <FeatureItem
{feature}{" "} key={feature.name}
{isExperimentalFeature(feature) ? ( feature={feature}
<Label color="orange"> />
{t("experimental")}
</Label>
) : null}
{isPreviewFeature(feature) ? (
<Label color="blue">{t("preview")}</Label>
) : null}
</ListItem>
))} ))}
</List> </List>
</DescriptionListDescription> </DescriptionListDescription>
@ -241,22 +235,10 @@ const Dashboard = () => {
<DescriptionListDescription> <DescriptionListDescription>
<List variant={ListVariant.inline}> <List variant={ListVariant.inline}>
{disabledFeatures.map((feature) => ( {disabledFeatures.map((feature) => (
<ListItem key={feature} className="pf-u-mb-sm"> <FeatureItem
{feature}{" "} key={feature.name}
{isExperimentalFeature(feature) ? ( feature={feature}
<Label color="orange"> />
{t("experimental")}
</Label>
) : null}
{isPreviewFeature(feature) ? (
<Label color="blue">{t("preview")}</Label>
) : null}
{isSupportedFeature(feature) ? (
<Label color="green">
{t("supported")}
</Label>
) : null}
</ListItem>
))} ))}
</List> </List>
</DescriptionListDescription> </DescriptionListDescription>

View file

@ -10,10 +10,15 @@ export enum Feature {
} }
export default function useIsFeatureEnabled() { export default function useIsFeatureEnabled() {
const { profileInfo } = useServerInfo(); const { features } = useServerInfo();
const disabledFilters = profileInfo?.disabledFeatures ?? [];
return function isFeatureEnabled(feature: Feature) { 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);
}; };
} }

View file

@ -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",
}

View file

@ -1,5 +1,6 @@
import type ComponentTypeRepresentation from "./componentTypeRepresentation.js"; import type ComponentTypeRepresentation from "./componentTypeRepresentation.js";
import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js"; import type { ConfigPropertyRepresentation } from "./configPropertyRepresentation.js";
import FeatureRepresentation from "./featureRepresentation.js";
import type PasswordPolicyTypeRepresentation from "./passwordPolicyTypeRepresentation.js"; import type PasswordPolicyTypeRepresentation from "./passwordPolicyTypeRepresentation.js";
import type ProfileInfoRepresentation from "./profileInfoRepresentation.js"; import type ProfileInfoRepresentation from "./profileInfoRepresentation.js";
import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js"; import type ProtocolMapperRepresentation from "./protocolMapperRepresentation.js";
@ -12,6 +13,7 @@ export interface ServerInfoRepresentation {
systemInfo?: SystemInfoRepresentation; systemInfo?: SystemInfoRepresentation;
memoryInfo?: MemoryInfoRepresentation; memoryInfo?: MemoryInfoRepresentation;
profileInfo?: ProfileInfoRepresentation; profileInfo?: ProfileInfoRepresentation;
features?: FeatureRepresentation[];
cryptoInfo?: CryptoInfoRepresentation; cryptoInfo?: CryptoInfoRepresentation;
themes?: { [index: string]: ThemeInfoRepresentation[] }; themes?: { [index: string]: ThemeInfoRepresentation[] };
socialProviders?: { [index: string]: string }[]; socialProviders?: { [index: string]: string }[];

View file

@ -49,6 +49,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation; import org.keycloak.representations.idm.ProtocolMapperTypeRepresentation;
import org.keycloak.representations.info.ClientInstallationRepresentation; import org.keycloak.representations.info.ClientInstallationRepresentation;
import org.keycloak.representations.info.CryptoInfoRepresentation; import org.keycloak.representations.info.CryptoInfoRepresentation;
import org.keycloak.representations.info.FeatureRepresentation;
import org.keycloak.representations.info.MemoryInfoRepresentation; import org.keycloak.representations.info.MemoryInfoRepresentation;
import org.keycloak.representations.info.ProfileInfoRepresentation; import org.keycloak.representations.info.ProfileInfoRepresentation;
import org.keycloak.representations.info.ProviderRepresentation; import org.keycloak.representations.info.ProviderRepresentation;
@ -104,6 +105,7 @@ public class ServerInfoAdminResource {
info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp())); info.setSystemInfo(SystemInfoRepresentation.create(session.getKeycloakSessionFactory().getServerStartupTimestamp()));
info.setMemoryInfo(MemoryInfoRepresentation.create()); info.setMemoryInfo(MemoryInfoRepresentation.create());
info.setProfileInfo(ProfileInfoRepresentation.create()); info.setProfileInfo(ProfileInfoRepresentation.create());
info.setFeatures(FeatureRepresentation.create());
// True - asymmetric algorithms, false - symmetric algorithms // True - asymmetric algorithms, false - symmetric algorithms
Map<Boolean, List<String>> algorithms = session.getAllProviders(ClientSignatureVerifierProvider.class).stream() Map<Boolean, List<String>> algorithms = session.getAllProviders(ClientSignatureVerifierProvider.class).stream()