374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
|
import {
|
|
AlertVariant,
|
|
Button,
|
|
ButtonVariant,
|
|
Dropdown,
|
|
DropdownItem,
|
|
KebabToggle,
|
|
ToolbarItem,
|
|
} from "@patternfly/react-core";
|
|
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router-dom";
|
|
|
|
import { adminClient } from "../../admin-client";
|
|
import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
|
|
import {
|
|
SearchDropdown,
|
|
SearchToolbar,
|
|
SearchType,
|
|
nameFilter,
|
|
typeFilter,
|
|
} from "../../client-scopes/details/SearchFilter";
|
|
import { useAlerts } from "../../components/alert/Alerts";
|
|
import {
|
|
AllClientScopeType,
|
|
AllClientScopes,
|
|
CellDropdown,
|
|
ClientScope,
|
|
addClientScope,
|
|
changeClientScope,
|
|
removeClientScope,
|
|
} from "../../components/client-scope/ClientScopeTypes";
|
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
|
import {
|
|
Action,
|
|
KeycloakDataTable,
|
|
} from "../../components/table-toolbar/KeycloakDataTable";
|
|
import { useAccess } from "../../context/access/Access";
|
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
|
import { toDedicatedScope } from "../routes/DedicatedScopeDetails";
|
|
import { AddScopeDialog } from "./AddScopeDialog";
|
|
|
|
import "./client-scopes.css";
|
|
|
|
export type ClientScopesProps = {
|
|
clientId: string;
|
|
protocol: string;
|
|
clientName: string;
|
|
fineGrainedAccess?: boolean;
|
|
};
|
|
|
|
export type Row = ClientScopeRepresentation & {
|
|
type: AllClientScopeType;
|
|
description?: string;
|
|
};
|
|
|
|
const DEDICATED_ROW = "dedicated";
|
|
|
|
type TypeSelectorProps = Row & {
|
|
clientId: string;
|
|
fineGrainedAccess?: boolean;
|
|
refresh: () => void;
|
|
};
|
|
|
|
const TypeSelector = ({
|
|
clientId,
|
|
refresh,
|
|
fineGrainedAccess,
|
|
...scope
|
|
}: TypeSelectorProps) => {
|
|
const { t } = useTranslation();
|
|
const { addAlert, addError } = useAlerts();
|
|
|
|
const { hasAccess } = useAccess();
|
|
|
|
const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW;
|
|
const isManager = hasAccess("manage-clients") || fineGrainedAccess;
|
|
|
|
return (
|
|
<CellDropdown
|
|
isDisabled={isDedicatedRow(scope) || !isManager}
|
|
clientScope={scope}
|
|
type={scope.type}
|
|
onSelect={async (value) => {
|
|
try {
|
|
await changeClientScope(
|
|
clientId,
|
|
scope,
|
|
scope.type,
|
|
value as ClientScope,
|
|
);
|
|
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
|
refresh();
|
|
} catch (error) {
|
|
addError("clientScopeError", error);
|
|
}
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export const ClientScopes = ({
|
|
clientId,
|
|
protocol,
|
|
clientName,
|
|
fineGrainedAccess,
|
|
}: ClientScopesProps) => {
|
|
const { t } = useTranslation();
|
|
const { addAlert, addError } = useAlerts();
|
|
const { realm } = useRealm();
|
|
const localeSort = useLocaleSort();
|
|
|
|
const [searchType, setSearchType] = useState<SearchType>("name");
|
|
|
|
const [searchTypeType, setSearchTypeType] = useState<AllClientScopes>(
|
|
AllClientScopes.none,
|
|
);
|
|
|
|
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
|
|
const [rest, setRest] = useState<ClientScopeRepresentation[]>();
|
|
const [selectedRows, setSelectedRowState] = useState<Row[]>([]);
|
|
const setSelectedRows = (rows: Row[]) =>
|
|
setSelectedRowState(rows.filter(({ id }) => id !== DEDICATED_ROW));
|
|
const [kebabOpen, setKebabOpen] = useState(false);
|
|
|
|
const [key, setKey] = useState(0);
|
|
const refresh = () => setKey(key + 1);
|
|
const isDedicatedRow = (value: Row) => value.id === DEDICATED_ROW;
|
|
|
|
const { hasAccess } = useAccess();
|
|
const isManager = hasAccess("manage-clients") || fineGrainedAccess;
|
|
const isViewer = hasAccess("view-clients") || fineGrainedAccess;
|
|
|
|
const loader = async (first?: number, max?: number, search?: string) => {
|
|
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!);
|
|
const row: Row = {
|
|
...c,
|
|
type: ClientScope.optional,
|
|
description: scope?.description,
|
|
};
|
|
return row;
|
|
});
|
|
|
|
const defaultScopes = defaultClientScopes.map((c) => {
|
|
const scope = find(c.id!);
|
|
const row: Row = {
|
|
...c,
|
|
type: ClientScope.default,
|
|
description: scope?.description,
|
|
};
|
|
return 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),
|
|
);
|
|
|
|
const filter =
|
|
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
|
|
const firstNum = Number(first);
|
|
const page = localeSort(rows.filter(filter), mapByKey("name"));
|
|
|
|
if (isViewer) {
|
|
page.unshift({
|
|
id: DEDICATED_ROW,
|
|
name: t("dedicatedScopeName", { clientName }),
|
|
type: AllClientScopes.none,
|
|
description: t("dedicatedScopeDescription"),
|
|
});
|
|
}
|
|
|
|
return page.slice(firstNum, firstNum + Number(max));
|
|
};
|
|
|
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
|
titleKey: t("deleteClientScope", {
|
|
count: selectedRows.length,
|
|
name: selectedRows[0]?.name,
|
|
}),
|
|
messageKey: "deleteConfirmClientScopes",
|
|
continueButtonLabel: "delete",
|
|
continueButtonVariant: ButtonVariant.danger,
|
|
onConfirm: async () => {
|
|
try {
|
|
await removeClientScope(
|
|
clientId,
|
|
selectedRows[0],
|
|
selectedRows[0].type as ClientScope,
|
|
);
|
|
addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success);
|
|
refresh();
|
|
} catch (error) {
|
|
addError("clientScopeRemoveError", error);
|
|
}
|
|
},
|
|
});
|
|
|
|
return (
|
|
<>
|
|
{rest && (
|
|
<AddScopeDialog
|
|
clientScopes={rest}
|
|
clientName={clientName!}
|
|
open={addDialogOpen}
|
|
toggleDialog={() => setAddDialogOpen(!addDialogOpen)}
|
|
onAdd={async (scopes) => {
|
|
try {
|
|
await Promise.all(
|
|
scopes.map(
|
|
async (scope) =>
|
|
await addClientScope(clientId, scope.scope, scope.type!),
|
|
),
|
|
);
|
|
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
|
refresh();
|
|
} catch (error) {
|
|
addError("clientScopeError", error);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
<KeycloakDataTable
|
|
key={key}
|
|
loader={loader}
|
|
ariaLabelKey={`clients:clientScopeList-${key}`}
|
|
searchPlaceholderKey={
|
|
searchType === "name" ? "searchByName" : undefined
|
|
}
|
|
canSelectAll
|
|
isPaginated
|
|
isSearching={searchType === "type"}
|
|
onSelect={(rows) => setSelectedRows([...rows])}
|
|
searchTypeComponent={
|
|
<SearchDropdown
|
|
searchType={searchType}
|
|
onSelect={(searchType) => setSearchType(searchType)}
|
|
/>
|
|
}
|
|
toolbarItem={
|
|
<>
|
|
<SearchToolbar
|
|
searchType={searchType}
|
|
type={searchTypeType}
|
|
onSelect={(searchType) => setSearchType(searchType)}
|
|
onType={(value) => {
|
|
setSearchTypeType(value);
|
|
refresh();
|
|
}}
|
|
/>
|
|
{isManager && (
|
|
<>
|
|
<DeleteConfirm />
|
|
<ToolbarItem>
|
|
<Button onClick={() => setAddDialogOpen(true)}>
|
|
{t("addClientScope")}
|
|
</Button>
|
|
</ToolbarItem>
|
|
<ToolbarItem>
|
|
<ChangeTypeDropdown
|
|
clientId={clientId}
|
|
selectedRows={selectedRows}
|
|
refresh={refresh}
|
|
/>
|
|
</ToolbarItem>
|
|
<ToolbarItem>
|
|
<Dropdown
|
|
toggle={
|
|
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
|
}
|
|
isOpen={kebabOpen}
|
|
isPlain
|
|
dropdownItems={[
|
|
<DropdownItem
|
|
key="deleteAll"
|
|
isDisabled={selectedRows.length === 0}
|
|
onClick={async () => {
|
|
try {
|
|
await Promise.all(
|
|
selectedRows.map((row) =>
|
|
removeClientScope(
|
|
clientId,
|
|
{ ...row },
|
|
row.type as ClientScope,
|
|
),
|
|
),
|
|
);
|
|
|
|
setKebabOpen(false);
|
|
setSelectedRows([]);
|
|
addAlert(t("clientScopeRemoveSuccess"));
|
|
refresh();
|
|
} catch (error) {
|
|
addError("clientScopeRemoveError", error);
|
|
}
|
|
}}
|
|
>
|
|
{t("remove")}
|
|
</DropdownItem>,
|
|
]}
|
|
/>
|
|
</ToolbarItem>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
columns={[
|
|
{
|
|
name: "name",
|
|
displayKey: "assignedClientScope",
|
|
cellRenderer: (row) => {
|
|
if (isDedicatedRow(row)) {
|
|
return (
|
|
<Link to={toDedicatedScope({ realm, clientId })}>
|
|
{row.name}
|
|
</Link>
|
|
);
|
|
}
|
|
return row.name!;
|
|
},
|
|
},
|
|
{
|
|
name: "type",
|
|
displayKey: "assignedType",
|
|
cellRenderer: (row) => (
|
|
<TypeSelector clientId={clientId} refresh={refresh} {...row} />
|
|
),
|
|
},
|
|
{ name: "description" },
|
|
]}
|
|
actions={
|
|
isManager
|
|
? [
|
|
{
|
|
title: t("remove"),
|
|
onRowClick: async (row) => {
|
|
setSelectedRows([row]);
|
|
toggleDeleteDialog();
|
|
return true;
|
|
},
|
|
} as Action<Row>,
|
|
]
|
|
: []
|
|
}
|
|
emptyState={
|
|
<ListEmptyState
|
|
message={t("emptyClientScopes")}
|
|
instructions={t("emptyClientScopesInstructions")}
|
|
primaryActionText={t("emptyClientScopesPrimaryAction")}
|
|
onPrimaryAction={() => setAddDialogOpen(true)}
|
|
/>
|
|
}
|
|
/>
|
|
</>
|
|
);
|
|
};
|