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

View file

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

View file

@ -83,12 +83,12 @@ public class SessionsResource {
switch (sessionId.getType()) { switch (sessionId.getType()) {
case REGULAR: case REGULAR:
result = Stream.concat(result, session.sessions().getUserSessionsStream(realm, clientModel) 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; break;
case OFFLINE: case OFFLINE:
result = Stream.concat(result, session.sessions() result = Stream.concat(result, session.sessions()
.getOfflineUserSessionsStream(realm, clientModel, Math.max((int) (first - clientSessionsCount), 0), max) .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; break;
} }
} }

View file

@ -4,6 +4,7 @@ import org.keycloak.admin.ui.rest.model.SessionId.SessionType;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
public class SessionRepresentation { public class SessionRepresentation {
private String id; private String id;
@ -79,5 +80,18 @@ public class SessionRepresentation {
public void setClients(Map<String, String> clients) { public void setClients(Map<String, String> clients) {
this.clients = 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);
}
} }