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