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
14 changes: 7 additions & 7 deletions common/src/main/java/org/keycloak/common/Profile.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public enum Feature {
LINKEDIN_OAUTH("LinkedIn Social Identity Provider based on OAuth", Type.DEPRECATED);

private final Type type;
private String label;
private final String label;

private Set<Feature> dependencies;
Feature(String label, Type type) {
Expand Down Expand Up @@ -131,7 +131,7 @@ public enum Type {
EXPERIMENTAL("Experimental"),
DEPRECATED("Deprecated");

private String label;
private final String label;

Type(String label) {
this.label = label;
Expand Down Expand Up @@ -246,7 +246,7 @@ private static Boolean isFeatureEnabled(ProfileName profile, Feature feature, Pr
case DEFAULT:
return true;
case PREVIEW:
return profile.equals(ProfileName.PREVIEW) ? true : false;
return profile.equals(ProfileName.PREVIEW);
default:
return false;
}
Expand All @@ -266,12 +266,12 @@ private static void verifyConfig(Map<Feature, Boolean> features) {
}

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<Feature> checkedFeatures, Logger.Level level) {
private void logUnsupportedFeatures(Feature.Type type, Set<Feature> checkedFeatures, Logger.Level level) {
Set<Feature.Type> checkedFeatureTypes = checkedFeatures.stream()
.map(Feature::getType)
.collect(Collectors.toSet());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public class ServerInfoRepresentation {
private MemoryInfoRepresentation memoryInfo;
private ProfileInfoRepresentation profileInfo;

private List<FeatureRepresentation> features;

private CryptoInfoRepresentation cryptoInfo;

private Map<String, List<ThemeInfoRepresentation>> themes;
Expand Down Expand Up @@ -77,6 +79,14 @@ public void setProfileInfo(ProfileInfoRepresentation profileInfo) {
this.profileInfo = profileInfo;
}

public List<FeatureRepresentation> getFeatures() {
return features;
}

public void setFeatures(List<FeatureRepresentation> features) {
this.features = features;
}

public CryptoInfoRepresentation getCryptoInfo() {
return cryptoInfo;
}
Expand Down
102 changes: 42 additions & 60 deletions js/apps/admin-ui/src/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { union, filter } from "lodash-es";
import {
Brand,
Card,
Expand All @@ -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,
Expand Down Expand Up @@ -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 { t } = useTranslation("dashboard");
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) =>
Expand Down Expand Up @@ -215,17 +216,10 @@ const Dashboard = () => {
<DescriptionListDescription>
<List variant={ListVariant.inline}>
{enabledFeatures.map((feature) => (
<ListItem key={feature} className="pf-u-mb-sm">
{feature}{" "}
{isExperimentalFeature(feature) ? (
<Label color="orange">
{t("experimental")}
</Label>
) : null}
{isPreviewFeature(feature) ? (
<Label color="blue">{t("preview")}</Label>
) : null}
</ListItem>
<FeatureItem
key={feature.name}
feature={feature}
/>
))}
</List>
</DescriptionListDescription>
Expand All @@ -241,22 +235,10 @@ const Dashboard = () => {
<DescriptionListDescription>
<List variant={ListVariant.inline}>
{disabledFeatures.map((feature) => (
<ListItem key={feature} className="pf-u-mb-sm">
{feature}{" "}
{isExperimentalFeature(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>
<FeatureItem
key={feature.name}
feature={feature}
/>
))}
</List>
</DescriptionListDescription>
Expand Down
11 changes: 8 additions & 3 deletions js/apps/admin-ui/src/utils/useIsFeatureEnabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,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);
};
}
16 changes: 16 additions & 0 deletions js/libs/keycloak-admin-client/src/defs/featureRepresentation.ts
Original file line number Diff line number Diff line change
@@ -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",
}
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 }[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
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;
Expand Down Expand Up @@ -104,6 +105,7 @@ public ServerInfoRepresentation getInfo() {
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<Boolean, List<String>> algorithms = session.getAllProviders(ClientSignatureVerifierProvider.class).stream()
Expand Down