Use Admin API extension to group sessions (#19837)

Fixes #19673
This commit is contained in:
Erik Jan de Wit 2023-04-26 10:17:58 +02:00 committed by GitHub
parent 52eeac76e1
commit 1f51ddb86e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 40 additions and 50 deletions

View file

@ -1,4 +1,3 @@
import { ClientSessionStat } from "@keycloak/keycloak-admin-client/lib/defs/clientSessionStat";
import {
DropdownItem,
PageSection,
@ -8,11 +7,13 @@ import {
import { useState } from "react";
import { useTranslation } from "react-i18next";
import UserSessionRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userSessionRepresentation";
import { FilterIcon } from "@patternfly/react-icons";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAdminClient } from "../context/auth/AdminClient";
import { fetchAdminUI } from "../context/auth/admin-ui-endpoint";
import { useRealm } from "../context/realm-context/RealmContext";
import helpUrls from "../help-urls";
import { RevocationModal } from "./RevocationModal";
@ -20,7 +21,7 @@ import SessionsTable from "./SessionsTable";
import "./SessionsSection.css";
type FilterType = "all" | "regular" | "offline";
type FilterType = "ALL" | "REGULAR" | "OFFLINE";
export default function SessionsSection() {
const { t } = useTranslation("sessions");
@ -33,55 +34,26 @@ export default function SessionsSection() {
const [revocationModalOpen, setRevocationModalOpen] = useState(false);
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
const [filterType, setFilterType] = useState<FilterType>("all");
const [filterType, setFilterType] = useState<FilterType>("ALL");
const [noSessions, setNoSessions] = useState(false);
const handleRevocationModalToggle = () => {
setRevocationModalOpen(!revocationModalOpen);
};
async function getClientSessions(clientSessionStats: ClientSessionStat[]) {
const sessions = await Promise.all(
clientSessionStats.map((client) =>
adminClient.clients.listSessions({ id: client.id })
)
);
return sessions.flat();
const loader = async (first?: number, max?: number, search?: string) => {
const data = await fetchAdminUI<UserSessionRepresentation[]>(
adminClient,
"ui-ext/sessions",
{
first: `${first}`,
max: `${max}`,
type: filterType,
search: search || "",
}
async function getOfflineSessions(clientSessionStats: ClientSessionStat[]) {
const sessions = await Promise.all(
clientSessionStats.map((client) =>
adminClient.clients.listOfflineSessions({ id: client.id })
)
);
return sessions.flat();
}
const loader = async () => {
const clientSessionStats = await adminClient.realms.getClientSessionStats({
realm,
});
const [clientSessions, offlineSessions] = await Promise.all([
filterType !== "offline" ? getClientSessions(clientSessionStats) : [],
filterType !== "regular" ? getOfflineSessions(clientSessionStats) : [],
]);
setNoSessions(clientSessions.length === 0 && offlineSessions.length === 0);
return [
...clientSessions.map((s) => ({
type: t("sessionsType.regularSSO"),
...s,
})),
...offlineSessions.map((s) => ({
type: t("sessionsType.offline"),
...s,
})),
];
setNoSessions(data.length === 0);
return data;
};
const [toggleLogoutDialog, LogoutConfirm] = useConfirmDialog({
@ -139,6 +111,7 @@ export default function SessionsSection() {
<SessionsTable
key={key}
loader={loader}
isSearching={filterType !== "ALL"}
filter={
<Select
data-testid="filter-session-type-select"
@ -147,18 +120,18 @@ export default function SessionsSection() {
toggleIcon={<FilterIcon />}
onSelect={(_, value) => {
setFilterType(value as FilterType);
refresh();
setFilterDropdownOpen(false);
refresh();
}}
selections={filterType}
>
<SelectOption data-testid="all-sessions-option" value="all">
<SelectOption data-testid="all-sessions-option" value="ALL">
{t("sessionsType.allSessions")}
</SelectOption>
<SelectOption data-testid="regular-sso-option" value="regular">
<SelectOption data-testid="regular-sso-option" value="REGULAR">
{t("sessionsType.regularSSO")}
</SelectOption>
<SelectOption data-testid="offline-option" value="offline">
<SelectOption data-testid="offline-option" value="OFFLINE">
{t("sessionsType.offline")}
</SelectOption>
</Select>

View file

@ -40,6 +40,7 @@ export type SessionsTableProps = {
emptyInstructions?: string;
logoutUser?: string;
filter?: ReactNode;
isSearching?: boolean;
};
const UsernameCell = (row: UserSessionRepresentation) => {
@ -72,6 +73,7 @@ export default function SessionsTable({
emptyInstructions,
logoutUser,
filter,
isSearching,
}: SessionsTableProps) {
const { realm } = useRealm();
const { whoAmI } = useWhoAmI();
@ -151,6 +153,7 @@ export default function SessionsTable({
loader={loader}
ariaLabelKey="sessions:title"
searchPlaceholderKey="sessions:searchForSession"
isSearching={isSearching}
searchTypeComponent={filter}
toolbarItem={
logoutUser && (

View file

@ -83,12 +83,12 @@ public class SessionsResource {
switch (sessionId.getType()) {
case REGULAR:
result = Stream.concat(result, session.sessions().getUserSessionsStream(realm, clientModel)
.map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), REGULAR)));
.map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), REGULAR))).distinct();
break;
case OFFLINE:
result = Stream.concat(result, session.sessions()
.getOfflineUserSessionsStream(realm, clientModel, Math.max((int) (first - clientSessionsCount), 0), max)
.map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), OFFLINE)));
.map(s -> toUserSessionRepresentation(s, sessionId.getClientId(), OFFLINE))).distinct();
break;
}
}

View file

@ -4,6 +4,7 @@ import org.keycloak.admin.ui.rest.model.SessionId.SessionType;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class SessionRepresentation {
private String id;
@ -79,5 +80,18 @@ public class SessionRepresentation {
public void setClients(Map<String, String> clients) {
this.clients = clients;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SessionRepresentation)) return false;
SessionRepresentation that = (SessionRepresentation) o;
return start == that.start && userId.equals(that.userId) && type == that.type;
}
@Override
public int hashCode() {
return Objects.hash(userId, start, type);
}
}