Sort enabled features (#2557)
This commit is contained in:
parent
58112be510
commit
93088c5380
12 changed files with 200 additions and 143 deletions
|
@ -65,11 +65,11 @@ describe("Authentication test", () => {
|
||||||
listingPage.goToItemDetails("Copy of browser");
|
listingPage.goToItemDetails("Copy of browser");
|
||||||
detailPage.addExecution(
|
detailPage.addExecution(
|
||||||
"Copy of browser forms",
|
"Copy of browser forms",
|
||||||
"console-username-password"
|
"reset-credentials-choose-user"
|
||||||
);
|
);
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Flow successfully updated");
|
masthead.checkNotificationMessage("Flow successfully updated");
|
||||||
detailPage.executionExists("Username Password Challenge");
|
detailPage.executionExists("Choose User");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add a condition", () => {
|
it("should add a condition", () => {
|
||||||
|
@ -80,7 +80,6 @@ describe("Authentication test", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Flow successfully updated");
|
masthead.checkNotificationMessage("Flow successfully updated");
|
||||||
detailPage.executionExists("Username Password Challenge");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add a sub-flow", () => {
|
it("should add a sub-flow", () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -9,10 +9,11 @@ import {
|
||||||
PageSection,
|
PageSection,
|
||||||
Radio,
|
Radio,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
|
||||||
|
|
||||||
import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar";
|
import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar";
|
||||||
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
|
import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
|
||||||
import { providerConditionFilter } from "../../FlowDetails";
|
import { providerConditionFilter } from "../../FlowDetails";
|
||||||
|
|
||||||
type AuthenticationProviderListProps = {
|
type AuthenticationProviderListProps = {
|
||||||
|
@ -64,6 +65,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
|
||||||
useState<AuthenticationProviderRepresentation[]>();
|
useState<AuthenticationProviderRepresentation[]>();
|
||||||
const [max, setMax] = useState(10);
|
const [max, setMax] = useState(10);
|
||||||
const [first, setFirst] = useState(0);
|
const [first, setFirst] = useState(0);
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -89,7 +91,14 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const page = providers?.slice(first, first + max + 1);
|
const page = useMemo(
|
||||||
|
() =>
|
||||||
|
localeSort(providers ?? [], mapByKey("displayName")).slice(
|
||||||
|
first,
|
||||||
|
first + max + 1
|
||||||
|
),
|
||||||
|
[providers, first, max]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -121,7 +130,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
|
||||||
>
|
>
|
||||||
{providers && providers.length > max && (
|
{providers && providers.length > max && (
|
||||||
<PaginatingTableToolbar
|
<PaginatingTableToolbar
|
||||||
count={page?.length || 0}
|
count={page.length || 0}
|
||||||
first={first}
|
first={first}
|
||||||
max={max}
|
max={max}
|
||||||
onNextClick={setFirst}
|
onNextClick={setFirst}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { emptyFormatter } from "../util";
|
import { emptyFormatter } from "../util";
|
||||||
|
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
|
||||||
import {
|
import {
|
||||||
CellDropdown,
|
CellDropdown,
|
||||||
ClientScope,
|
ClientScope,
|
||||||
|
@ -32,9 +33,7 @@ import {
|
||||||
import { ChangeTypeDropdown } from "./ChangeTypeDropdown";
|
import { ChangeTypeDropdown } from "./ChangeTypeDropdown";
|
||||||
import { toNewClientScope } from "./routes/NewClientScope";
|
import { toNewClientScope } from "./routes/NewClientScope";
|
||||||
|
|
||||||
import "./client-scope.css";
|
|
||||||
import { toClientScope } from "./routes/ClientScope";
|
import { toClientScope } from "./routes/ClientScope";
|
||||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
|
||||||
import {
|
import {
|
||||||
nameFilter,
|
nameFilter,
|
||||||
protocolFilter,
|
protocolFilter,
|
||||||
|
@ -48,9 +47,10 @@ import type { Row } from "../clients/scopes/ClientScopes";
|
||||||
import { getProtocolName } from "../clients/utils";
|
import { getProtocolName } from "../clients/utils";
|
||||||
import helpUrls from "../help-urls";
|
import helpUrls from "../help-urls";
|
||||||
|
|
||||||
|
import "./client-scope.css";
|
||||||
|
|
||||||
export default function ClientScopesSection() {
|
export default function ClientScopesSection() {
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const { t } = useTranslation("client-scopes");
|
const { t } = useTranslation("client-scopes");
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
@ -66,6 +66,7 @@ export default function ClientScopesSection() {
|
||||||
AllClientScopes.none
|
AllClientScopes.none
|
||||||
);
|
);
|
||||||
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all");
|
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all");
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => {
|
const refresh = () => {
|
||||||
|
@ -87,7 +88,7 @@ export default function ClientScopesSection() {
|
||||||
? typeFilter(searchTypeType)
|
? typeFilter(searchTypeType)
|
||||||
: protocolFilter(searchProtocol);
|
: protocolFilter(searchProtocol);
|
||||||
|
|
||||||
return clientScopes
|
const transformed = clientScopes
|
||||||
.map((scope) => {
|
.map((scope) => {
|
||||||
const row: Row = {
|
const row: Row = {
|
||||||
...scope,
|
...scope,
|
||||||
|
@ -103,9 +104,12 @@ export default function ClientScopesSection() {
|
||||||
};
|
};
|
||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
.filter(filter)
|
.filter(filter);
|
||||||
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
|
|
||||||
.slice(first, Number(first) + Number(max));
|
return localeSort(transformed, mapByKey("name")).slice(
|
||||||
|
first,
|
||||||
|
Number(first) + Number(max)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
|
|
@ -19,9 +19,9 @@ import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/l
|
||||||
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
||||||
|
|
||||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
|
||||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
|
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -46,28 +46,26 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => {
|
||||||
const { t } = useTranslation("client-scopes");
|
const { t } = useTranslation("client-scopes");
|
||||||
|
|
||||||
const serverInfo = useServerInfo();
|
const serverInfo = useServerInfo();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const protocol = props.protocol;
|
const protocol = props.protocol;
|
||||||
const protocolMappers = serverInfo.protocolMapperTypes![protocol];
|
const protocolMappers = serverInfo.protocolMapperTypes![protocol];
|
||||||
const builtInMappers = serverInfo.builtinProtocolMappers![protocol];
|
const builtInMappers = serverInfo.builtinProtocolMappers![protocol];
|
||||||
const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
|
const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
|
||||||
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
|
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const allRows = useMemo(
|
const allRows = useMemo(
|
||||||
() =>
|
() =>
|
||||||
builtInMappers
|
localeSort(builtInMappers, mapByKey("name")).map((mapper) => {
|
||||||
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
|
const mapperType = protocolMappers.filter(
|
||||||
.map((mapper) => {
|
(type) => type.id === mapper.protocolMapper
|
||||||
const mapperType = protocolMappers.filter(
|
)[0];
|
||||||
(type) => type.id === mapper.protocolMapper
|
return {
|
||||||
)[0];
|
item: mapper,
|
||||||
return {
|
name: mapper.name!,
|
||||||
item: mapper,
|
description: mapperType.helpText,
|
||||||
name: mapper.name!,
|
};
|
||||||
description: mapperType.helpText,
|
}),
|
||||||
};
|
[builtInMappers, protocolMappers]
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
);
|
||||||
const [rows, setRows] = useState(allRows);
|
const [rows, setRows] = useState(allRows);
|
||||||
|
|
||||||
|
@ -78,10 +76,7 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedProtocolMappers = useMemo(
|
const sortedProtocolMappers = useMemo(
|
||||||
() =>
|
() => localeSort(protocolMappers, mapByKey("name")),
|
||||||
protocolMappers.sort((a, b) =>
|
|
||||||
a.name!.localeCompare(b.name!, whoAmI.getLocale())
|
|
||||||
),
|
|
||||||
[protocolMappers]
|
[protocolMappers]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/
|
||||||
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
import { AddScopeDialog } from "../../scopes/AddScopeDialog";
|
import { AddScopeDialog } from "../../scopes/AddScopeDialog";
|
||||||
import { useWhoAmI } from "../../../context/whoami/WhoAmI";
|
import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
|
||||||
|
|
||||||
export type RequiredIdValue = {
|
export type RequiredIdValue = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -41,7 +41,7 @@ export const ClientScope = () => {
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { whoAmI } = useWhoAmI();
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
useFetch(
|
useFetch(
|
||||||
() => adminClient.clientScopes.find(),
|
() => adminClient.clientScopes.find(),
|
||||||
|
@ -49,11 +49,7 @@ export const ClientScope = () => {
|
||||||
setSelectedScopes(
|
setSelectedScopes(
|
||||||
getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!)
|
getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!)
|
||||||
);
|
);
|
||||||
setScopes(
|
setScopes(localeSort(scopes, mapByKey("name")));
|
||||||
scopes.sort((a, b) =>
|
|
||||||
a.name!.localeCompare(b.name!, whoAmI.getLocale())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
|
@ -36,7 +36,7 @@ import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
|
||||||
|
|
||||||
import { toDedicatedScope } from "../routes/DedicatedScopeDetails";
|
import { toDedicatedScope } from "../routes/DedicatedScopeDetails";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
||||||
|
|
||||||
import "./client-scopes.css";
|
import "./client-scopes.css";
|
||||||
|
|
||||||
|
@ -60,9 +60,9 @@ export const ClientScopes = ({
|
||||||
}: ClientScopesProps) => {
|
}: ClientScopesProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const [searchType, setSearchType] = useState<SearchType>("name");
|
const [searchType, setSearchType] = useState<SearchType>("name");
|
||||||
|
|
||||||
|
@ -116,13 +116,15 @@ export const ClientScopes = ({
|
||||||
clientScopes
|
clientScopes
|
||||||
.filter((scope) => !names.includes(scope.name))
|
.filter((scope) => !names.includes(scope.name))
|
||||||
.filter((scope) => scope.protocol === protocol)
|
.filter((scope) => scope.protocol === protocol)
|
||||||
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const filter =
|
const filter =
|
||||||
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
|
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
|
||||||
const firstNum = Number(first);
|
const firstNum = Number(first);
|
||||||
const page = rows.filter(filter).slice(firstNum, firstNum + Number(max));
|
const page = localeSort(rows.filter(filter), mapByKey("name")).slice(
|
||||||
|
firstNum,
|
||||||
|
firstNum + Number(max)
|
||||||
|
);
|
||||||
if (firstNum === 0) {
|
if (firstNum === 0) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition";
|
import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition";
|
||||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
||||||
|
|
||||||
export const MultivaluedScopesComponent = ({
|
export const MultivaluedScopesComponent = ({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
@ -22,8 +22,8 @@ export const MultivaluedScopesComponent = ({
|
||||||
const { control } = useFormContext();
|
const { control } = useFormContext();
|
||||||
const { conditionName } = useParams<EditClientPolicyConditionParams>();
|
const { conditionName } = useParams<EditClientPolicyConditionParams>();
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>(
|
const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>(
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
@ -62,11 +62,12 @@ export const MultivaluedScopesComponent = ({
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
<AddScopeDialog
|
<AddScopeDialog
|
||||||
clientScopes={clientScopes
|
clientScopes={localeSort(
|
||||||
.filter((scope) => !value.includes(scope.name!))
|
clientScopes.filter(
|
||||||
.sort((a, b) =>
|
(scope) => !value.includes(scope.name!)
|
||||||
a.name!.localeCompare(b.name!, whoAmI.getLocale())
|
),
|
||||||
)}
|
mapByKey("name")
|
||||||
|
)}
|
||||||
isClientScopesConditionType
|
isClientScopesConditionType
|
||||||
open={open}
|
open={open}
|
||||||
toggleDialog={() => setOpen(!open)}
|
toggleDialog={() => setOpen(!open)}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
import { toPermissionDetails } from "../../clients/routes/PermissionDetails";
|
import { toPermissionDetails } from "../../clients/routes/PermissionDetails";
|
||||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
import useLocaleSort from "../../utils/useLocaleSort";
|
||||||
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
|
||||||
|
|
||||||
import "./permissions-tab.css";
|
import "./permissions-tab.css";
|
||||||
|
@ -43,9 +43,9 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const [realmId, setRealmId] = useState("");
|
const [realmId, setRealmId] = useState("");
|
||||||
const [permission, setPermission] = useState<ManagementPermissionReference>();
|
const [permission, setPermission] = useState<ManagementPermissionReference>();
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const togglePermissionEnabled = (enabled: boolean) => {
|
const togglePermissionEnabled = (enabled: boolean) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
@ -173,48 +173,47 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{Object.entries(permission.scopePermissions || {})
|
{localeSort(
|
||||||
.sort((a, b) =>
|
Object.entries(permission.scopePermissions || {}),
|
||||||
a[0]!.localeCompare(b[0]!, whoAmI.getLocale())
|
([name]) => name
|
||||||
)
|
).map(([name, id]) => (
|
||||||
.map(([name, id]) => (
|
<Tr key={id}>
|
||||||
<Tr key={id}>
|
<Td>
|
||||||
<Td>
|
<Link
|
||||||
<Link
|
to={toPermissionDetails({
|
||||||
to={toPermissionDetails({
|
realm,
|
||||||
realm,
|
id: realmId,
|
||||||
id: realmId,
|
permissionType: "scope",
|
||||||
permissionType: "scope",
|
permissionId: id,
|
||||||
permissionId: id,
|
})}
|
||||||
})}
|
>
|
||||||
>
|
{name}
|
||||||
{name}
|
</Link>
|
||||||
</Link>
|
</Td>
|
||||||
</Td>
|
<Td>
|
||||||
<Td>
|
{t(`scopePermissions.${type}.${name}-description`)}
|
||||||
{t(`scopePermissions.${type}.${name}-description`)}
|
</Td>
|
||||||
</Td>
|
<Td isActionCell>
|
||||||
<Td isActionCell>
|
<ActionsColumn
|
||||||
<ActionsColumn
|
items={[
|
||||||
items={[
|
{
|
||||||
{
|
title: t("common:edit"),
|
||||||
title: t("common:edit"),
|
onClick() {
|
||||||
onClick() {
|
history.push(
|
||||||
history.push(
|
toPermissionDetails({
|
||||||
toPermissionDetails({
|
realm,
|
||||||
realm,
|
id: realmId,
|
||||||
id: realmId,
|
permissionType: "scope",
|
||||||
permissionType: "scope",
|
permissionId: id,
|
||||||
permissionId: id,
|
})
|
||||||
})
|
);
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
},
|
||||||
/>
|
]}
|
||||||
</Td>
|
/>
|
||||||
</Tr>
|
</Td>
|
||||||
))}
|
</Tr>
|
||||||
|
))}
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</TableComposable>
|
</TableComposable>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { xor } from "lodash-es";
|
import { xor } from "lodash-es";
|
||||||
|
@ -33,14 +33,15 @@ import { toUpperCase } from "../util";
|
||||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
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 {
|
import {
|
||||||
routableTab,
|
routableTab,
|
||||||
RoutableTabs,
|
RoutableTabs,
|
||||||
} from "../components/routable-tabs/RoutableTabs";
|
} from "../components/routable-tabs/RoutableTabs";
|
||||||
import { DashboardTab, toDashboard } from "./routes/Dashboard";
|
import { DashboardTab, toDashboard } from "./routes/Dashboard";
|
||||||
|
import { ProviderInfo } from "./ProviderInfo";
|
||||||
|
|
||||||
import "./dashboard.css";
|
import "./dashboard.css";
|
||||||
import { ProviderInfo } from "./ProviderInfo";
|
|
||||||
|
|
||||||
const EmptyDashboard = () => {
|
const EmptyDashboard = () => {
|
||||||
const { t } = useTranslation("dashboard");
|
const { t } = useTranslation("dashboard");
|
||||||
|
@ -70,11 +71,28 @@ const Dashboard = () => {
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const serverInfo = useServerInfo();
|
const serverInfo = useServerInfo();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const enabledFeatures = xor(
|
const enabledFeatures = useMemo(
|
||||||
serverInfo.profileInfo?.disabledFeatures,
|
() =>
|
||||||
serverInfo.profileInfo?.experimentalFeatures,
|
localeSort(
|
||||||
serverInfo.profileInfo?.previewFeatures
|
xor(
|
||||||
|
serverInfo.profileInfo?.disabledFeatures,
|
||||||
|
serverInfo.profileInfo?.experimentalFeatures,
|
||||||
|
serverInfo.profileInfo?.previewFeatures
|
||||||
|
),
|
||||||
|
(item) => item
|
||||||
|
),
|
||||||
|
[serverInfo.profileInfo]
|
||||||
|
);
|
||||||
|
|
||||||
|
const disabledFeatures = useMemo(
|
||||||
|
() =>
|
||||||
|
localeSort(
|
||||||
|
serverInfo.profileInfo?.disabledFeatures ?? [],
|
||||||
|
(item) => item
|
||||||
|
),
|
||||||
|
[serverInfo.profileInfo]
|
||||||
);
|
);
|
||||||
|
|
||||||
const isExperimentalFeature = (feature: string) =>
|
const isExperimentalFeature = (feature: string) =>
|
||||||
|
@ -193,11 +211,9 @@ const Dashboard = () => {
|
||||||
</DescriptionListTerm>
|
</DescriptionListTerm>
|
||||||
<DescriptionListDescription>
|
<DescriptionListDescription>
|
||||||
<List variant={ListVariant.inline}>
|
<List variant={ListVariant.inline}>
|
||||||
{serverInfo.profileInfo?.disabledFeatures?.map(
|
{disabledFeatures.map((feature) => (
|
||||||
(feature) => (
|
<ListItem key={feature}>{feature}</ListItem>
|
||||||
<ListItem key={feature}>{feature}</ListItem>
|
))}
|
||||||
)
|
|
||||||
)}
|
|
||||||
</List>
|
</List>
|
||||||
</DescriptionListDescription>
|
</DescriptionListDescription>
|
||||||
</DescriptionListGroup>
|
</DescriptionListGroup>
|
||||||
|
|
|
@ -12,46 +12,47 @@ import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/de
|
||||||
import { KeycloakDataTable } from "../../../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../../../components/table-toolbar/KeycloakDataTable";
|
||||||
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
|
||||||
import { useAlerts } from "../../../components/alert/Alerts";
|
import { useAlerts } from "../../../components/alert/Alerts";
|
||||||
import { useAdminClient } from "../../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||||
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
|
||||||
import { useWhoAmI } from "../../../context/whoami/WhoAmI";
|
import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
|
||||||
|
|
||||||
export const LdapMapperList = () => {
|
export const LdapMapperList = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation("user-federation");
|
const { t } = useTranslation("user-federation");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { addAlert, addError } = useAlerts();
|
const { addAlert, addError } = useAlerts();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
const { url } = useRouteMatch();
|
const { url } = useRouteMatch();
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(key + 1);
|
const refresh = () => setKey(key + 1);
|
||||||
|
|
||||||
|
const [mappers, setMappers] = useState<ComponentRepresentation[]>([]);
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
|
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const [selectedMapper, setSelectedMapper] =
|
const [selectedMapper, setSelectedMapper] =
|
||||||
useState<ComponentRepresentation>();
|
useState<ComponentRepresentation>();
|
||||||
|
|
||||||
const loader = async () => {
|
useFetch(
|
||||||
const testParams: {
|
() =>
|
||||||
[name: string]: string | number;
|
adminClient.components.find({
|
||||||
} = {
|
parent: id,
|
||||||
parent: id,
|
type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
|
||||||
type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
|
}),
|
||||||
};
|
(mapper) => {
|
||||||
|
setMappers(
|
||||||
const mappersList = (await adminClient.components.find(testParams)).map(
|
localeSort(
|
||||||
(mapper) => {
|
mapper.map((mapper) => ({
|
||||||
return {
|
...mapper,
|
||||||
...mapper,
|
name: mapper.name,
|
||||||
name: mapper.name,
|
type: mapper.providerId,
|
||||||
type: mapper.providerId,
|
})),
|
||||||
} as ComponentRepresentation;
|
mapByKey("name")
|
||||||
}
|
)
|
||||||
);
|
);
|
||||||
return mappersList.sort((a, b) =>
|
},
|
||||||
a.name!.localeCompare(b.name!, whoAmI.getLocale())
|
[key]
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
titleKey: t("common:deleteMappingTitle", { mapperId: selectedMapper?.id }),
|
titleKey: t("common:deleteMappingTitle", { mapperId: selectedMapper?.id }),
|
||||||
|
@ -88,7 +89,7 @@ export const LdapMapperList = () => {
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
key={key}
|
key={key}
|
||||||
loader={loader}
|
loader={mappers}
|
||||||
ariaLabelKey="ldapMappersList"
|
ariaLabelKey="ldapMappersList"
|
||||||
searchPlaceholderKey="common:searchForMapper"
|
searchPlaceholderKey="common:searchForMapper"
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
|
|
|
@ -10,8 +10,8 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
|
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
|
||||||
import { useWhoAmI } from "../../context/whoami/WhoAmI";
|
|
||||||
import useToggle from "../../utils/useToggle";
|
import useToggle from "../../utils/useToggle";
|
||||||
|
import useLocaleSort from "../../utils/useLocaleSort";
|
||||||
import { CredentialDataDialog } from "./CredentialDataDialog";
|
import { CredentialDataDialog } from "./CredentialDataDialog";
|
||||||
|
|
||||||
type CredentialRowProps = {
|
type CredentialRowProps = {
|
||||||
|
@ -30,8 +30,7 @@ export const CredentialRow = ({
|
||||||
const { t } = useTranslation("users");
|
const { t } = useTranslation("users");
|
||||||
const [showData, toggleShow] = useToggle();
|
const [showData, toggleShow] = useToggle();
|
||||||
const [kebabOpen, toggleKebab] = useToggle();
|
const [kebabOpen, toggleKebab] = useToggle();
|
||||||
|
const localeSort = useLocaleSort();
|
||||||
const { whoAmI } = useWhoAmI();
|
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo(() => {
|
||||||
if (!credential.credentialData) {
|
if (!credential.credentialData) {
|
||||||
|
@ -41,17 +40,15 @@ export const CredentialRow = ({
|
||||||
const credentialData: Record<string, unknown> = JSON.parse(
|
const credentialData: Record<string, unknown> = JSON.parse(
|
||||||
credential.credentialData
|
credential.credentialData
|
||||||
);
|
);
|
||||||
const locale = whoAmI.getLocale();
|
return localeSort(Object.entries(credentialData), ([key]) => key).map<
|
||||||
|
[string, string]
|
||||||
|
>(([key, value]) => {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return [key, value];
|
||||||
|
}
|
||||||
|
|
||||||
return Object.entries(credentialData)
|
return [key, JSON.stringify(value)];
|
||||||
.sort(([a], [b]) => a.localeCompare(b, locale))
|
});
|
||||||
.map<[string, string]>(([key, value]) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return [key, value];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [key, JSON.stringify(value)];
|
|
||||||
});
|
|
||||||
}, [credential.credentialData]);
|
}, [credential.credentialData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
38
src/utils/useLocaleSort.ts
Normal file
38
src/utils/useLocaleSort.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||||
|
|
||||||
|
export type ValueMapperFn<T> = (item: T) => string | undefined;
|
||||||
|
|
||||||
|
export default function useLocaleSort() {
|
||||||
|
const { whoAmI } = useWhoAmI();
|
||||||
|
|
||||||
|
return function localeSort<T>(items: T[], mapperFn: ValueMapperFn<T>): T[] {
|
||||||
|
const locale = whoAmI.getLocale();
|
||||||
|
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
const valA = mapperFn(a);
|
||||||
|
const valB = mapperFn(b);
|
||||||
|
|
||||||
|
if (valA === undefined || valB === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valA.localeCompare(valB, locale);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This might be built into TypeScript into future.
|
||||||
|
// See: https://github.com/microsoft/TypeScript/issues/48992
|
||||||
|
type KeysMatching<T, V> = {
|
||||||
|
[K in keyof T]: T[K] extends V ? K : never;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export const mapByKey =
|
||||||
|
<
|
||||||
|
T extends { [_ in K]?: string },
|
||||||
|
K extends KeysMatching<T, string | undefined>
|
||||||
|
>(
|
||||||
|
key: K
|
||||||
|
) =>
|
||||||
|
(item: T) =>
|
||||||
|
item[key];
|
Loading…
Reference in a new issue