changed to use the keycloak datatable (#432)
* changed to use the keycloak datatable so it will benifid from a general way tables work in the admin console * null check * changed to use any active tab seems sometimes the tab doesn't get set properly so instead of "mappers" it's still "settings" this change makes it work with both
This commit is contained in:
parent
b56788d942
commit
dfc4beced4
4 changed files with 345 additions and 465 deletions
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useHistory, useRouteMatch } from "react-router-dom";
|
import { Link, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
|
@ -8,12 +8,6 @@ import {
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableHeader,
|
|
||||||
TableVariant,
|
|
||||||
} from "@patternfly/react-table";
|
|
||||||
import { CaretDownIcon } from "@patternfly/react-icons";
|
import { CaretDownIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||||
|
@ -21,19 +15,18 @@ import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapper
|
||||||
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
|
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
|
||||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
|
|
||||||
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
|
|
||||||
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 { AddMapperDialog } from "../add/MapperDialog";
|
import { AddMapperDialog } from "../add/MapperDialog";
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
|
|
||||||
type MapperListProps = {
|
type MapperListProps = {
|
||||||
clientScope: ClientScopeRepresentation;
|
clientScope: ClientScopeRepresentation;
|
||||||
refresh: () => void;
|
refresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Row = {
|
type Row = ProtocolMapperRepresentation & {
|
||||||
name: JSX.Element;
|
|
||||||
category: string;
|
category: string;
|
||||||
type: string;
|
type: string;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
@ -46,15 +39,15 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { url } = useRouteMatch();
|
const { url } = useRouteMatch();
|
||||||
|
|
||||||
const [filteredData, setFilteredData] = useState<
|
|
||||||
{ mapper: ProtocolMapperRepresentation; cells: Row }[]
|
|
||||||
>();
|
|
||||||
const [mapperAction, setMapperAction] = useState(false);
|
const [mapperAction, setMapperAction] = useState(false);
|
||||||
const mapperList = clientScope.protocolMappers!;
|
const mapperList = clientScope.protocolMappers!;
|
||||||
const mapperTypes = useServerInfo().protocolMapperTypes![
|
const mapperTypes = useServerInfo().protocolMapperTypes![
|
||||||
clientScope.protocol!
|
clientScope.protocol!
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
useEffect(() => setKey(new Date().getTime()), [mapperList]);
|
||||||
|
|
||||||
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
|
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
|
||||||
const [filter, setFilter] = useState(clientScope.protocolMappers);
|
const [filter, setFilter] = useState(clientScope.protocolMappers);
|
||||||
const toggleAddMapperDialog = (buildIn: boolean) => {
|
const toggleAddMapperDialog = (buildIn: boolean) => {
|
||||||
|
@ -86,98 +79,31 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!mapperList) {
|
const loader = async () =>
|
||||||
return (
|
Promise.resolve(
|
||||||
<>
|
(mapperList || [])
|
||||||
<AddMapperDialog
|
.map((mapper) => {
|
||||||
protocol={clientScope.protocol!}
|
const mapperType = mapperTypes.filter(
|
||||||
filter={filter}
|
(type) => type.id === mapper.protocolMapper
|
||||||
onConfirm={addMappers}
|
)[0];
|
||||||
open={addMapperDialogOpen}
|
return {
|
||||||
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
...mapper,
|
||||||
/>
|
category: mapperType.category,
|
||||||
<ListEmptyState
|
type: mapperType.name,
|
||||||
message={t("emptyMappers")}
|
priority: mapperType.priority,
|
||||||
instructions={t("emptyMappersInstructions")}
|
} as Row;
|
||||||
primaryActionText={t("emptyPrimaryAction")}
|
})
|
||||||
onPrimaryAction={() => toggleAddMapperDialog(true)}
|
.sort((a, b) => a.priority - b.priority)
|
||||||
secondaryActions={[
|
|
||||||
{
|
|
||||||
text: t("emptySecondaryAction"),
|
|
||||||
onClick: () => toggleAddMapperDialog(false),
|
|
||||||
type: ButtonVariant.secondary,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
const data = mapperList
|
const MapperLink = (mapper: Row) => (
|
||||||
.map((mapper) => {
|
<>
|
||||||
const mapperType = mapperTypes.filter(
|
<Link to={`${url}/${mapper.id}`}>{mapper.name}</Link>
|
||||||
(type) => type.id === mapper.protocolMapper
|
</>
|
||||||
)[0];
|
);
|
||||||
return {
|
|
||||||
mapper,
|
|
||||||
cells: {
|
|
||||||
name: (
|
|
||||||
<>
|
|
||||||
<Link to={`${url}/${mapper.id}`}>{mapper.name}</Link>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
category: mapperType.category,
|
|
||||||
type: mapperType.name,
|
|
||||||
priority: mapperType.priority,
|
|
||||||
} as Row,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => a.cells.priority - b.cells.priority);
|
|
||||||
|
|
||||||
const filterData = (search: string) => {
|
|
||||||
setFilteredData(
|
|
||||||
data.filter((column) =>
|
|
||||||
column.mapper.name!.toLowerCase().includes(search.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableToolbar
|
<>
|
||||||
inputGroupName="clientsScopeToolbarTextInput"
|
|
||||||
inputGroupPlaceholder={t("mappersSearchFor")}
|
|
||||||
inputGroupOnChange={filterData}
|
|
||||||
toolbarItem={
|
|
||||||
<Dropdown
|
|
||||||
onSelect={() => setMapperAction(false)}
|
|
||||||
toggle={
|
|
||||||
<DropdownToggle
|
|
||||||
isPrimary
|
|
||||||
id="mapperAction"
|
|
||||||
onToggle={() => setMapperAction(!mapperAction)}
|
|
||||||
toggleIndicator={CaretDownIcon}
|
|
||||||
>
|
|
||||||
{t("addMapper")}
|
|
||||||
</DropdownToggle>
|
|
||||||
}
|
|
||||||
isOpen={mapperAction}
|
|
||||||
dropdownItems={[
|
|
||||||
<DropdownItem
|
|
||||||
key="predefined"
|
|
||||||
onClick={() => toggleAddMapperDialog(true)}
|
|
||||||
>
|
|
||||||
{t("fromPredefinedMapper")}
|
|
||||||
</DropdownItem>,
|
|
||||||
<DropdownItem
|
|
||||||
key="byConfiguration"
|
|
||||||
onClick={() => toggleAddMapperDialog(false)}
|
|
||||||
>
|
|
||||||
{t("byConfiguration")}
|
|
||||||
</DropdownItem>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AddMapperDialog
|
<AddMapperDialog
|
||||||
protocol={clientScope.protocol!}
|
protocol={clientScope.protocol!}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
@ -185,42 +111,92 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
|
||||||
open={addMapperDialogOpen}
|
open={addMapperDialogOpen}
|
||||||
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
||||||
/>
|
/>
|
||||||
<Table
|
|
||||||
variant={TableVariant.compact}
|
<KeycloakDataTable
|
||||||
cells={[
|
key={key}
|
||||||
t("common:name"),
|
loader={loader}
|
||||||
t("common:category"),
|
ariaLabelKey="client-scopes:clientScopeList"
|
||||||
t("common:type"),
|
searchPlaceholderKey="client-scopes:mappersSearchFor"
|
||||||
t("common:priority"),
|
toolbarItem={
|
||||||
]}
|
<Dropdown
|
||||||
rows={(filteredData || data).map((cell) => {
|
onSelect={() => setMapperAction(false)}
|
||||||
return { cells: Object.values(cell.cells), mapper: cell.mapper };
|
toggle={
|
||||||
})}
|
<DropdownToggle
|
||||||
aria-label={t("clientScopeList")}
|
isPrimary
|
||||||
|
id="mapperAction"
|
||||||
|
onToggle={() => setMapperAction(!mapperAction)}
|
||||||
|
toggleIndicator={CaretDownIcon}
|
||||||
|
>
|
||||||
|
{t("addMapper")}
|
||||||
|
</DropdownToggle>
|
||||||
|
}
|
||||||
|
isOpen={mapperAction}
|
||||||
|
dropdownItems={[
|
||||||
|
<DropdownItem
|
||||||
|
key="predefined"
|
||||||
|
onClick={() => toggleAddMapperDialog(true)}
|
||||||
|
>
|
||||||
|
{t("fromPredefinedMapper")}
|
||||||
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
key="byConfiguration"
|
||||||
|
onClick={() => toggleAddMapperDialog(false)}
|
||||||
|
>
|
||||||
|
{t("byConfiguration")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
title: t("common:delete"),
|
title: t("common:delete"),
|
||||||
onClick: async (_, rowId) => {
|
onRowClick: async (mapper) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.clientScopes.delProtocolMapper({
|
await adminClient.clientScopes.delProtocolMapper({
|
||||||
id: clientScope.id!,
|
id: clientScope.id!,
|
||||||
mapperId: data[rowId].mapper.id!,
|
mapperId: mapper.id!,
|
||||||
});
|
});
|
||||||
refresh();
|
|
||||||
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
|
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
|
||||||
|
refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("mappingDeletedError", { error }),
|
t("mappingDeletedError", { error }),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
>
|
columns={[
|
||||||
<TableHeader />
|
{
|
||||||
<TableBody />
|
name: "name",
|
||||||
</Table>
|
cellRenderer: MapperLink,
|
||||||
</TableToolbar>
|
},
|
||||||
|
{ name: "category" },
|
||||||
|
{
|
||||||
|
name: "type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
emptyState={
|
||||||
|
<ListEmptyState
|
||||||
|
message={t("emptyMappers")}
|
||||||
|
instructions={t("emptyMappersInstructions")}
|
||||||
|
primaryActionText={t("emptyPrimaryAction")}
|
||||||
|
onPrimaryAction={() => toggleAddMapperDialog(true)}
|
||||||
|
secondaryActions={[
|
||||||
|
{
|
||||||
|
text: t("emptySecondaryAction"),
|
||||||
|
onClick: () => toggleAddMapperDialog(false),
|
||||||
|
type: ButtonVariant.secondary,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,14 +1,5 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useErrorHandler } from "react-error-boundary";
|
|
||||||
import {
|
|
||||||
IFormatter,
|
|
||||||
IFormatterValueType,
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableHeader,
|
|
||||||
TableVariant,
|
|
||||||
} from "@patternfly/react-table";
|
|
||||||
import {
|
import {
|
||||||
AlertVariant,
|
AlertVariant,
|
||||||
Button,
|
Button,
|
||||||
|
@ -17,19 +8,14 @@ import {
|
||||||
DropdownToggle,
|
DropdownToggle,
|
||||||
KebabToggle,
|
KebabToggle,
|
||||||
Select,
|
Select,
|
||||||
Spinner,
|
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { FilterIcon } from "@patternfly/react-icons";
|
import { FilterIcon } from "@patternfly/react-icons";
|
||||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||||
import KeycloakAdminClient from "keycloak-admin";
|
import KeycloakAdminClient from "keycloak-admin";
|
||||||
|
|
||||||
import {
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
useAdminClient,
|
|
||||||
asyncStateFetch,
|
|
||||||
} from "../../context/auth/AdminClient";
|
|
||||||
import { toUpperCase } from "../../util";
|
import { toUpperCase } from "../../util";
|
||||||
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
|
|
||||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
import { AddScopeDialog } from "./AddScopeDialog";
|
import { AddScopeDialog } from "./AddScopeDialog";
|
||||||
import {
|
import {
|
||||||
|
@ -38,12 +24,18 @@ import {
|
||||||
ClientScope,
|
ClientScope,
|
||||||
} from "./ClientScopeTypes";
|
} from "./ClientScopeTypes";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
|
|
||||||
export type ClientScopesProps = {
|
export type ClientScopesProps = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Row = ClientScopeRepresentation & {
|
||||||
|
type: ClientScopeType;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
const castAdminClient = (adminClient: KeycloakAdminClient) =>
|
const castAdminClient = (adminClient: KeycloakAdminClient) =>
|
||||||
(adminClient.clients as unknown) as {
|
(adminClient.clients as unknown) as {
|
||||||
[index: string]: Function;
|
[index: string]: Function;
|
||||||
|
@ -124,7 +116,6 @@ type TableRow = {
|
||||||
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const handleError = useErrorHandler();
|
|
||||||
const { addAlert } = useAlerts();
|
const { addAlert } = useAlerts();
|
||||||
|
|
||||||
const [searchToggle, setSearchToggle] = useState(false);
|
const [searchToggle, setSearchToggle] = useState(false);
|
||||||
|
@ -133,100 +124,73 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
const [kebabOpen, setKebabOpen] = useState(false);
|
const [kebabOpen, setKebabOpen] = useState(false);
|
||||||
|
|
||||||
const [rows, setRows] = useState<TableRow[]>();
|
|
||||||
const [rest, setRest] = useState<ClientScopeRepresentation[]>();
|
const [rest, setRest] = useState<ClientScopeRepresentation[]>();
|
||||||
|
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(new Date().getTime());
|
const refresh = () => setKey(new Date().getTime());
|
||||||
|
|
||||||
useEffect(() => {
|
const loader = async () => {
|
||||||
return asyncStateFetch(
|
const defaultClientScopes = await adminClient.clients.listDefaultClientScopes(
|
||||||
async () => {
|
{ id: clientId }
|
||||||
const defaultClientScopes = await adminClient.clients.listDefaultClientScopes(
|
|
||||||
{ id: clientId }
|
|
||||||
);
|
|
||||||
const optionalClientScopes = await adminClient.clients.listOptionalClientScopes(
|
|
||||||
{ id: clientId }
|
|
||||||
);
|
|
||||||
const clientScopes = await adminClient.clientScopes.find();
|
|
||||||
|
|
||||||
const find = (id: string) =>
|
|
||||||
clientScopes.find((clientScope) => id === clientScope.id)!;
|
|
||||||
|
|
||||||
const optional = optionalClientScopes.map((c) => {
|
|
||||||
const scope = find(c.id!);
|
|
||||||
return {
|
|
||||||
selected: false,
|
|
||||||
clientScope: c,
|
|
||||||
type: ClientScope.optional,
|
|
||||||
cells: [c.name, c.id, scope.description],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultScopes = defaultClientScopes.map((c) => {
|
|
||||||
const scope = find(c.id!);
|
|
||||||
return {
|
|
||||||
selected: false,
|
|
||||||
clientScope: c,
|
|
||||||
type: ClientScope.default,
|
|
||||||
cells: [c.name, c.id, scope.description],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = [...optional, ...defaultScopes];
|
|
||||||
const names = rows.map((row) => row.cells[0]);
|
|
||||||
|
|
||||||
const rest = clientScopes
|
|
||||||
.filter((scope) => !names.includes(scope.name))
|
|
||||||
.filter((scope) => scope.protocol === protocol);
|
|
||||||
return { rows, rest };
|
|
||||||
},
|
|
||||||
({ rows, rest }) => {
|
|
||||||
setRows(rows);
|
|
||||||
setRest(rest);
|
|
||||||
},
|
|
||||||
handleError
|
|
||||||
);
|
);
|
||||||
}, [key]);
|
const optionalClientScopes = await adminClient.clients.listOptionalClientScopes(
|
||||||
|
{ id: clientId }
|
||||||
|
);
|
||||||
|
const clientScopes = await adminClient.clientScopes.find();
|
||||||
|
|
||||||
const dropdown = (): IFormatter => (data?: IFormatterValueType) => {
|
const find = (id: string) =>
|
||||||
if (!data) {
|
clientScopes.find((clientScope) => id === clientScope.id)!;
|
||||||
return <></>;
|
|
||||||
}
|
const optional = optionalClientScopes.map((c) => {
|
||||||
const row = rows?.find((row) => row.clientScope.id === data.toString())!;
|
const scope = find(c.id!);
|
||||||
return (
|
return {
|
||||||
|
...c,
|
||||||
|
type: ClientScope.optional,
|
||||||
|
description: scope.description,
|
||||||
|
} as Row;
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultScopes = defaultClientScopes.map((c) => {
|
||||||
|
const scope = find(c.id!);
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
type: ClientScope.default,
|
||||||
|
description: scope.description,
|
||||||
|
} as Row;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = [...optional, ...defaultScopes];
|
||||||
|
const names = rows.map((row) => row.name);
|
||||||
|
setRest(
|
||||||
|
clientScopes
|
||||||
|
.filter((scope) => !names.includes(scope.name))
|
||||||
|
.filter((scope) => scope.protocol === protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TypeSelector = (scope: Row) => (
|
||||||
|
<>
|
||||||
<CellDropdown
|
<CellDropdown
|
||||||
clientScope={row.clientScope}
|
clientScope={scope}
|
||||||
type={row.type}
|
type={scope.type}
|
||||||
onSelect={async (value) => {
|
onSelect={async (value) => {
|
||||||
try {
|
try {
|
||||||
await changeScope(
|
await changeScope(adminClient, clientId, scope, scope.type, value);
|
||||||
adminClient,
|
|
||||||
clientId,
|
|
||||||
row.clientScope,
|
|
||||||
row.type,
|
|
||||||
value
|
|
||||||
);
|
|
||||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||||
await refresh();
|
refresh();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(t("clientScopeError", { error }), AlertVariant.danger);
|
addAlert(t("clientScopeError", { error }), AlertVariant.danger);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
</>
|
||||||
};
|
);
|
||||||
|
|
||||||
const filterData = () => {};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!rows && (
|
|
||||||
<div className="pf-u-text-align-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{rest && (
|
{rest && (
|
||||||
<AddScopeDialog
|
<AddScopeDialog
|
||||||
clientScopes={rest}
|
clientScopes={rest}
|
||||||
|
@ -254,202 +218,154 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{rows && rows.length > 0 && (
|
<KeycloakDataTable
|
||||||
<TableToolbar
|
key={key}
|
||||||
searchTypeComponent={
|
loader={loader}
|
||||||
<Dropdown
|
ariaLabelKey="clients:clientScopeList"
|
||||||
toggle={
|
searchPlaceholderKey="clients:searchByName"
|
||||||
<DropdownToggle
|
onSelect={(rows) => setSelectedRows([...rows])}
|
||||||
id="toggle-id"
|
searchTypeComponent={
|
||||||
onToggle={() => setSearchToggle(!searchToggle)}
|
<Dropdown
|
||||||
>
|
toggle={
|
||||||
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
|
<DropdownToggle
|
||||||
</DropdownToggle>
|
id="toggle-id"
|
||||||
}
|
onToggle={() => setSearchToggle(!searchToggle)}
|
||||||
aria-label="Select Input"
|
>
|
||||||
isOpen={searchToggle}
|
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
|
||||||
dropdownItems={[
|
</DropdownToggle>
|
||||||
<DropdownItem
|
}
|
||||||
key="client"
|
aria-label="Select Input"
|
||||||
onClick={() => {
|
isOpen={searchToggle}
|
||||||
setSearchType("client");
|
dropdownItems={[
|
||||||
setSearchToggle(false);
|
<DropdownItem
|
||||||
}}
|
key="client"
|
||||||
>
|
onClick={() => {
|
||||||
{t("clientScopeSearch.client")}
|
setSearchType("client");
|
||||||
</DropdownItem>,
|
setSearchToggle(false);
|
||||||
<DropdownItem
|
}}
|
||||||
key="assigned"
|
>
|
||||||
onClick={() => {
|
{t("clientScopeSearch.client")}
|
||||||
setSearchType("assigned");
|
</DropdownItem>,
|
||||||
setSearchToggle(false);
|
<DropdownItem
|
||||||
}}
|
key="assigned"
|
||||||
>
|
onClick={() => {
|
||||||
{t("clientScopeSearch.assigned")}
|
setSearchType("assigned");
|
||||||
</DropdownItem>,
|
setSearchToggle(false);
|
||||||
]}
|
}}
|
||||||
/>
|
>
|
||||||
}
|
{t("clientScopeSearch.assigned")}
|
||||||
inputGroupName="clientsScopeToolbarTextInput"
|
</DropdownItem>,
|
||||||
inputGroupPlaceholder={t("searchByName")}
|
|
||||||
inputGroupOnChange={filterData}
|
|
||||||
toolbarItem={
|
|
||||||
<>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Button onClick={() => setAddDialogOpen(true)}>
|
|
||||||
{t("addClientScope")}
|
|
||||||
</Button>
|
|
||||||
</ToolbarItem>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Select
|
|
||||||
id="add-dropdown"
|
|
||||||
key="add-dropdown"
|
|
||||||
isOpen={addToggle}
|
|
||||||
selections={[]}
|
|
||||||
placeholderText={t("changeTypeTo")}
|
|
||||||
onToggle={() => setAddToggle(!addToggle)}
|
|
||||||
onSelect={async (_, value) => {
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
rows.map((row) => {
|
|
||||||
if (row.selected) {
|
|
||||||
return changeScope(
|
|
||||||
adminClient,
|
|
||||||
clientId,
|
|
||||||
row.clientScope,
|
|
||||||
row.type,
|
|
||||||
value as ClientScope
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setAddToggle(false);
|
|
||||||
await refresh();
|
|
||||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
|
||||||
} catch (error) {
|
|
||||||
addAlert(
|
|
||||||
t("clientScopeError", { error }),
|
|
||||||
AlertVariant.danger
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{clientScopeTypesSelectOptions(t)}
|
|
||||||
</Select>
|
|
||||||
</ToolbarItem>
|
|
||||||
<ToolbarItem>
|
|
||||||
<Dropdown
|
|
||||||
onSelect={() => {}}
|
|
||||||
toggle={
|
|
||||||
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
|
||||||
}
|
|
||||||
isOpen={kebabOpen}
|
|
||||||
isPlain
|
|
||||||
dropdownItems={[
|
|
||||||
<DropdownItem
|
|
||||||
key="deleteAll"
|
|
||||||
isDisabled={
|
|
||||||
rows.filter((row) => row.selected).length === 0
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
rows.map(async (row) => {
|
|
||||||
if (row.selected) {
|
|
||||||
await removeScope(
|
|
||||||
adminClient,
|
|
||||||
clientId,
|
|
||||||
row.clientScope,
|
|
||||||
row.type
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setKebabOpen(false);
|
|
||||||
addAlert(
|
|
||||||
t("clientScopeRemoveSuccess"),
|
|
||||||
AlertVariant.success
|
|
||||||
);
|
|
||||||
refresh();
|
|
||||||
} catch (error) {
|
|
||||||
addAlert(
|
|
||||||
t("clientScopeRemoveError", { error }),
|
|
||||||
AlertVariant.danger
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("common:remove")}
|
|
||||||
</DropdownItem>,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</ToolbarItem>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
onSelect={(_, isSelected, rowIndex) => {
|
|
||||||
if (rowIndex === -1) {
|
|
||||||
setRows(
|
|
||||||
rows.map((row) => {
|
|
||||||
row.selected = isSelected;
|
|
||||||
return row;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
rows[rowIndex].selected = isSelected;
|
|
||||||
setRows([...rows]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
variant={TableVariant.compact}
|
|
||||||
cells={[
|
|
||||||
t("common:name"),
|
|
||||||
{ title: t("assignedType"), cellFormatters: [dropdown()] },
|
|
||||||
t("common:description"),
|
|
||||||
]}
|
]}
|
||||||
rows={rows}
|
/>
|
||||||
actions={[
|
}
|
||||||
{
|
toolbarItem={
|
||||||
title: t("common:remove"),
|
<>
|
||||||
onClick: async (_, rowId) => {
|
<ToolbarItem>
|
||||||
|
<Button onClick={() => setAddDialogOpen(true)}>
|
||||||
|
{t("addClientScope")}
|
||||||
|
</Button>
|
||||||
|
</ToolbarItem>
|
||||||
|
<ToolbarItem>
|
||||||
|
<Select
|
||||||
|
id="add-dropdown"
|
||||||
|
key="add-dropdown"
|
||||||
|
isOpen={addToggle}
|
||||||
|
selections={[]}
|
||||||
|
placeholderText={t("changeTypeTo")}
|
||||||
|
onToggle={() => setAddToggle(!addToggle)}
|
||||||
|
onSelect={async (_, value) => {
|
||||||
try {
|
try {
|
||||||
await removeScope(
|
await Promise.all(
|
||||||
adminClient,
|
selectedRows.map((row) => {
|
||||||
clientId,
|
return changeScope(
|
||||||
rows[rowId].clientScope,
|
adminClient,
|
||||||
rows[rowId].type
|
clientId,
|
||||||
);
|
{ ...row },
|
||||||
addAlert(
|
row.type,
|
||||||
t("clientScopeRemoveSuccess"),
|
value as ClientScope
|
||||||
AlertVariant.success
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
setAddToggle(false);
|
||||||
refresh();
|
refresh();
|
||||||
|
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("clientScopeRemoveError", { error }),
|
t("clientScopeError", { error }),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
}}
|
||||||
},
|
>
|
||||||
]}
|
{clientScopeTypesSelectOptions(t)}
|
||||||
aria-label={t("clientScopeList")}
|
</Select>
|
||||||
>
|
</ToolbarItem>
|
||||||
<TableHeader />
|
<ToolbarItem>
|
||||||
<TableBody />
|
<Dropdown
|
||||||
</Table>
|
onSelect={() => {}}
|
||||||
</TableToolbar>
|
toggle={
|
||||||
)}
|
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
||||||
{rows && rows.length === 0 && (
|
}
|
||||||
<ListEmptyState
|
isOpen={kebabOpen}
|
||||||
message={t("clients:emptyClientScopes")}
|
isPlain
|
||||||
instructions={t("clients:emptyClientScopesInstructions")}
|
dropdownItems={[
|
||||||
primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
|
<DropdownItem
|
||||||
onPrimaryAction={() => setAddDialogOpen(true)}
|
key="deleteAll"
|
||||||
/>
|
isDisabled={selectedRows.length === 0}
|
||||||
)}
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
selectedRows.map(async (row) => {
|
||||||
|
await removeScope(
|
||||||
|
adminClient,
|
||||||
|
clientId,
|
||||||
|
{ ...row },
|
||||||
|
row.type
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setKebabOpen(false);
|
||||||
|
addAlert(
|
||||||
|
t("clientScopeRemoveSuccess"),
|
||||||
|
AlertVariant.success
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(
|
||||||
|
t("clientScopeRemoveError", { error }),
|
||||||
|
AlertVariant.danger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("common:remove")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
name: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "type",
|
||||||
|
displayKey: "clients:assignedType",
|
||||||
|
cellRenderer: TypeSelector,
|
||||||
|
},
|
||||||
|
{ name: "description" },
|
||||||
|
]}
|
||||||
|
emptyState={
|
||||||
|
<ListEmptyState
|
||||||
|
message={t("clients:emptyClientScopes")}
|
||||||
|
instructions={t("clients:emptyClientScopesInstructions")}
|
||||||
|
primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
|
||||||
|
onPrimaryAction={() => setAddDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
import React, { Fragment, useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableHeader,
|
|
||||||
TableVariant,
|
|
||||||
} from "@patternfly/react-table";
|
|
||||||
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
|
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
|
||||||
|
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
|
||||||
import { DataLoader } from "../../components/data-loader/DataLoader";
|
|
||||||
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
|
|
||||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
|
||||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||||
|
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
|
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||||
|
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||||
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
import { emptyFormatter } from "../../util";
|
import { emptyFormatter } from "../../util";
|
||||||
|
|
||||||
import "./service-account.css";
|
import "./service-account.css";
|
||||||
|
@ -21,6 +15,11 @@ type ServiceAccountProps = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Row = {
|
||||||
|
client: ClientRepresentation;
|
||||||
|
role: CompositeRole;
|
||||||
|
};
|
||||||
|
|
||||||
type CompositeRole = RoleRepresentation & {
|
type CompositeRole = RoleRepresentation & {
|
||||||
parent: RoleRepresentation;
|
parent: RoleRepresentation;
|
||||||
};
|
};
|
||||||
|
@ -66,6 +65,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
||||||
|
console.log(clientRolesFlat);
|
||||||
|
|
||||||
const addInherentData = await (async () =>
|
const addInherentData = await (async () =>
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
@ -92,35 +92,34 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
] as CompositeRole[])
|
] as CompositeRole[])
|
||||||
.sort((r1, r2) => r1.name!.localeCompare(r2.name!))
|
.sort((r1, r2) => r1.name!.localeCompare(r2.name!))
|
||||||
.map((role) => {
|
.map((role) => {
|
||||||
const client = findClient(role);
|
|
||||||
return {
|
return {
|
||||||
cells: [
|
client: findClient(role),
|
||||||
<Fragment key={role.id}>
|
role,
|
||||||
{client && (
|
} as Row;
|
||||||
<Badge
|
|
||||||
key={client.id}
|
|
||||||
isRead
|
|
||||||
className="keycloak-admin--service-account__client-name"
|
|
||||||
>
|
|
||||||
{client.clientId}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{role.name}
|
|
||||||
</Fragment>,
|
|
||||||
role.parent ? role.parent.name : "",
|
|
||||||
role.description,
|
|
||||||
],
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterData = () => {};
|
const RoleLink = ({ role, client }: Row) => (
|
||||||
|
<>
|
||||||
|
{client && (
|
||||||
|
<Badge
|
||||||
|
key={client.id}
|
||||||
|
isRead
|
||||||
|
className="keycloak-admin--service-account__client-name"
|
||||||
|
>
|
||||||
|
{client.clientId}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{role.name}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableToolbar
|
<KeycloakDataTable
|
||||||
inputGroupName="clientsServiceAccountRoleToolbarTextInput"
|
loader={loader}
|
||||||
inputGroupPlaceholder={t("searchByName")}
|
onSelect={() => {}}
|
||||||
inputGroupOnChange={filterData}
|
searchPlaceholderKey="clients:searchByName"
|
||||||
|
ariaLabelKey="clients:clientScopeList"
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
|
@ -136,34 +135,23 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
columns={[
|
||||||
<DataLoader loader={loader} deps={[clientId]}>
|
{
|
||||||
{(clientRoles) => (
|
name: "role.name",
|
||||||
<>
|
displayKey: t("name"),
|
||||||
{hide ? "" : " "}
|
cellRenderer: RoleLink,
|
||||||
<Table
|
},
|
||||||
onSelect={() => {}}
|
{
|
||||||
variant={TableVariant.compact}
|
name: "role.parent.name",
|
||||||
cells={[
|
displayKey: t("inherentFrom"),
|
||||||
t("roles:roleName"),
|
cellFormatters: [emptyFormatter()],
|
||||||
{
|
},
|
||||||
title: t("inherentFrom"),
|
{
|
||||||
cellFormatters: [emptyFormatter()],
|
name: "role.description",
|
||||||
},
|
displayKey: t("description"),
|
||||||
{
|
cellFormatters: [emptyFormatter()],
|
||||||
title: t("common:description"),
|
},
|
||||||
cellFormatters: [emptyFormatter()],
|
]}
|
||||||
},
|
/>
|
||||||
]}
|
|
||||||
rows={clientRoles}
|
|
||||||
aria-label="roleList"
|
|
||||||
>
|
|
||||||
<TableHeader />
|
|
||||||
<TableBody />
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DataLoader>
|
|
||||||
</TableToolbar>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -92,13 +92,13 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
access: "manage-clients",
|
access: "manage-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/client-scopes/:id/mappers/oidc-role-name-mapper",
|
path: "/:realm/client-scopes/:id/:tab/oidc-role-name-mapper",
|
||||||
component: RoleMappingForm,
|
component: RoleMappingForm,
|
||||||
breadcrumb: t("client-scopes:mappingDetails"),
|
breadcrumb: t("client-scopes:mappingDetails"),
|
||||||
access: "view-clients",
|
access: "view-clients",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/client-scopes/:id/mappers/:mapperId",
|
path: "/:realm/client-scopes/:id/:tab/:mapperId",
|
||||||
component: MappingDetails,
|
component: MappingDetails,
|
||||||
breadcrumb: t("client-scopes:mappingDetails"),
|
breadcrumb: t("client-scopes:mappingDetails"),
|
||||||
access: "view-clients",
|
access: "view-clients",
|
||||||
|
|
Loading…
Reference in a new issue