commit
d90b1842ce
11 changed files with 210 additions and 164 deletions
|
@ -19,7 +19,11 @@ import { convertFormValuesToObject } from "../../util";
|
|||
import { MapperList } from "../details/MapperList";
|
||||
import { ScopeForm } from "../details/ScopeForm";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { RoleMapping, Row } from "../../components/role-mapping/RoleMapping";
|
||||
import {
|
||||
mapRoles,
|
||||
RoleMapping,
|
||||
Row,
|
||||
} from "../../components/role-mapping/RoleMapping";
|
||||
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import {
|
||||
AllClientScopes,
|
||||
|
@ -61,41 +65,35 @@ export const ClientScopeForm = () => {
|
|||
);
|
||||
|
||||
const loader = async () => {
|
||||
const assignedRoles = hide
|
||||
? await adminClient.clientScopes.listRealmScopeMappings({ id })
|
||||
: await adminClient.clientScopes.listCompositeRealmScopeMappings({ id });
|
||||
const assignedRoles = (
|
||||
await adminClient.clientScopes.listRealmScopeMappings({ id })
|
||||
).map((role) => ({ role }));
|
||||
const effectiveRoles = (
|
||||
await adminClient.clientScopes.listCompositeRealmScopeMappings({ id })
|
||||
).map((role) => ({ role }));
|
||||
const clients = await adminClient.clients.find();
|
||||
|
||||
const clientRoles = (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const clientScope = hide
|
||||
? await adminClient.clientScopes.listClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
})
|
||||
: await adminClient.clientScopes.listCompositeClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
});
|
||||
return clientScope.map((scope) => {
|
||||
return {
|
||||
client,
|
||||
role: scope,
|
||||
};
|
||||
});
|
||||
const clientAssignedRoles = (
|
||||
await adminClient.clientScopes.listClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
})
|
||||
).map((role) => ({ role, client }));
|
||||
const clientEffectiveRoles = (
|
||||
await adminClient.clientScopes.listCompositeClientScopeMappings({
|
||||
id,
|
||||
client: client.id!,
|
||||
})
|
||||
).map((role) => ({ role, client }));
|
||||
return mapRoles(clientAssignedRoles, clientEffectiveRoles, hide);
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
|
||||
return [
|
||||
...assignedRoles.map((role) => {
|
||||
return {
|
||||
role,
|
||||
};
|
||||
}),
|
||||
...clientRoles,
|
||||
];
|
||||
return [...mapRoles(assignedRoles, effectiveRoles, hide), ...clientRoles];
|
||||
};
|
||||
|
||||
const save = async (clientScopes: ClientScopeDefaultOptionalType) => {
|
||||
|
|
|
@ -81,6 +81,7 @@ const ClientDetailHeader = ({
|
|||
subKey="clients:clientsExplain"
|
||||
badge={client.protocol}
|
||||
divider={false}
|
||||
helpTextKey="clients-help:enableDisable"
|
||||
dropdownItems={[
|
||||
<DropdownItem key="download" onClick={() => toggleDownloadDialog()}>
|
||||
{t("downloadAdapterConfig")}
|
||||
|
@ -358,7 +359,7 @@ export const ClientDetails = () => {
|
|||
eventKey="serviceAccount"
|
||||
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
||||
>
|
||||
<ServiceAccount clientId={clientId} />
|
||||
<ServiceAccount client={client} />
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"clients-help": {
|
||||
"enableDisable": "Disabled clients cannot initiate a login or have obtained access tokens.",
|
||||
"clientType": "'OpenID connect' allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server.'SAML' enables web-based authentication and authorization scenarios including cross-domain single sign-on (SSO) and uses security tokens containing assertions to pass information.",
|
||||
"serviceAccount": "Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.",
|
||||
"authentication": "This defines the type of the OIDC client. When it's ON, the OIDC type is set to confidential access type. When it's OFF, it is set to public access type",
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
"authenticationFlow": "Authentication flow",
|
||||
"standardFlow": "Standard flow",
|
||||
"directAccess": "Direct access grants",
|
||||
"serviceAccount": "Service accounts",
|
||||
"serviceAccount": "Service accounts roles",
|
||||
"enableServiceAccount": "Enable service account roles",
|
||||
"assignRolesTo": "Assign roles to {{client}} account",
|
||||
"searchByRoleName": "Search by role name",
|
||||
|
|
|
@ -1,99 +1,74 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertVariant } from "@patternfly/react-core";
|
||||
|
||||
import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
||||
import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import {
|
||||
CompositeRole,
|
||||
mapRoles,
|
||||
RoleMapping,
|
||||
Row,
|
||||
} from "../../components/role-mapping/RoleMapping";
|
||||
|
||||
type ServiceAccountProps = {
|
||||
clientId: string;
|
||||
client: ClientRepresentation;
|
||||
};
|
||||
|
||||
export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||
export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [serviceAccountId, setServiceAccountId] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [serviceAccount, setServiceAccount] = useState<UserRepresentation>();
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
adminClient.clients.getServiceAccountUser({
|
||||
id: client.id!,
|
||||
}),
|
||||
(serviceAccount) => setServiceAccount(serviceAccount),
|
||||
[]
|
||||
);
|
||||
|
||||
const loader = async () => {
|
||||
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
||||
id: clientId,
|
||||
});
|
||||
setServiceAccountId(serviceAccount.id!);
|
||||
const effectiveRoles = await adminClient.users.listCompositeRealmRoleMappings(
|
||||
{ id: serviceAccount.id! }
|
||||
);
|
||||
const assignedRoles = await adminClient.users.listRealmRoleMappings({
|
||||
id: serviceAccount.id!,
|
||||
id: client.id!,
|
||||
});
|
||||
const id = serviceAccount.id!;
|
||||
|
||||
const assignedRoles = (
|
||||
await adminClient.users.listRealmRoleMappings({ id })
|
||||
).map((role) => ({ role }));
|
||||
const effectiveRoles = (
|
||||
await adminClient.users.listCompositeRealmRoleMappings({ id })
|
||||
).map((role) => ({ role }));
|
||||
|
||||
const clients = await adminClient.clients.find();
|
||||
setName(clients.find((c) => c.id === clientId)?.clientId!);
|
||||
const clientRoles = (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
return {
|
||||
client,
|
||||
roles: await adminClient.users.listClientRoleMappings({
|
||||
id: serviceAccount.id!,
|
||||
const clientAssignedRoles = (
|
||||
await adminClient.users.listClientRoleMappings({
|
||||
id,
|
||||
clientUniqueId: client.id!,
|
||||
}),
|
||||
};
|
||||
})
|
||||
).map((role) => ({ role, client }));
|
||||
const clientEffectiveRoles = (
|
||||
await adminClient.users.listCompositeClientRoleMappings({
|
||||
id,
|
||||
clientUniqueId: client.id!,
|
||||
})
|
||||
).map((role) => ({ role, client }));
|
||||
return mapRoles(clientAssignedRoles, clientEffectiveRoles, hide);
|
||||
})
|
||||
)
|
||||
).filter((rows) => rows.roles.length > 0);
|
||||
).flat();
|
||||
|
||||
const findClient = (role: RoleRepresentation) => {
|
||||
const row = clientRoles.filter((row) =>
|
||||
row.roles.find((r) => r.id === role.id)
|
||||
)[0];
|
||||
return row ? row.client : undefined;
|
||||
};
|
||||
|
||||
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
||||
|
||||
const addInherentData = await (async () =>
|
||||
Promise.all(
|
||||
effectiveRoles.map(async (role) => {
|
||||
const compositeRoles = await adminClient.roles.getCompositeRolesForRealm(
|
||||
{ realm, id: role.id! }
|
||||
);
|
||||
return compositeRoles.length > 0
|
||||
? compositeRoles.map((r) => {
|
||||
return { ...r, parent: role };
|
||||
})
|
||||
: { ...role, parent: undefined };
|
||||
})
|
||||
))();
|
||||
const uniqueRolesWithParent = addInherentData
|
||||
.flat()
|
||||
.filter(
|
||||
(role, index, array) =>
|
||||
array.findIndex((r) => r.id === role.id) === index
|
||||
);
|
||||
return ([
|
||||
...(hide ? assignedRoles : uniqueRolesWithParent),
|
||||
...clientRolesFlat,
|
||||
] as CompositeRole[])
|
||||
.sort((r1, r2) => r1.name!.localeCompare(r2.name!))
|
||||
.map((role) => {
|
||||
return {
|
||||
client: findClient(role),
|
||||
role,
|
||||
} as Row;
|
||||
});
|
||||
return [...mapRoles(assignedRoles, effectiveRoles, hide), ...clientRoles];
|
||||
};
|
||||
|
||||
const assignRoles = async (rows: Row[]) => {
|
||||
|
@ -103,7 +78,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
.map((row) => row.role as RoleMappingPayload)
|
||||
.flat();
|
||||
adminClient.users.addRealmRoleMappings({
|
||||
id: serviceAccountId,
|
||||
id: serviceAccount?.id!,
|
||||
roles: realmRoles,
|
||||
});
|
||||
await Promise.all(
|
||||
|
@ -111,7 +86,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
.filter((row) => row.client !== undefined)
|
||||
.map((row) =>
|
||||
adminClient.users.addClientRoleMappings({
|
||||
id: serviceAccountId,
|
||||
id: serviceAccount?.id!,
|
||||
clientUniqueId: row.client!.id!,
|
||||
roles: [row.role as RoleMappingPayload],
|
||||
})
|
||||
|
@ -128,13 +103,17 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
}
|
||||
};
|
||||
return (
|
||||
<RoleMapping
|
||||
name={name}
|
||||
id={serviceAccountId}
|
||||
type={"service-account"}
|
||||
loader={loader}
|
||||
save={assignRoles}
|
||||
onHideRolesToggle={() => setHide(!hide)}
|
||||
/>
|
||||
<>
|
||||
{serviceAccount && (
|
||||
<RoleMapping
|
||||
name={client.clientId!}
|
||||
id={serviceAccount.id!}
|
||||
type="service-account"
|
||||
loader={loader}
|
||||
save={assignRoles}
|
||||
onHideRolesToggle={() => setHide(!hide)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -248,60 +248,64 @@ export const AddRoleMappingModal = ({
|
|||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
toggleId="role"
|
||||
onToggle={() => setSearchToggle(!searchToggle)}
|
||||
isOpen={searchToggle}
|
||||
variant={isRadio ? SelectVariant.single : SelectVariant.checkbox}
|
||||
hasInlineFilter
|
||||
menuAppendTo="parent"
|
||||
placeholderText={
|
||||
<>
|
||||
<FilterIcon /> {t("filterByOrigin")}
|
||||
</>
|
||||
}
|
||||
isGrouped
|
||||
onFilter={(evt) => {
|
||||
const value = evt?.target.value || "";
|
||||
return createSelectGroup(
|
||||
clients.filter((client) => client.clientId?.includes(value))
|
||||
);
|
||||
}}
|
||||
selections={selectedClients}
|
||||
onClear={() => setSelectedClients([])}
|
||||
onSelect={(_, selection) => {
|
||||
const client = selection as ClientRole;
|
||||
if (selectedClients.includes(client)) {
|
||||
removeClient(client);
|
||||
} else {
|
||||
setSelectedClients([...selectedClients, client]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createSelectGroup(clients)}
|
||||
</Select>
|
||||
<ToolbarItem variant="chip-group">
|
||||
<ChipGroup>
|
||||
{selectedClients.map((client) => (
|
||||
<Chip
|
||||
key={`chip-${client.id}`}
|
||||
onClick={() => {
|
||||
removeClient(client);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
{client.clientId || t("realmRoles")}
|
||||
<Badge isRead={true}>{client.numberOfRoles}</Badge>
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</ToolbarItem>
|
||||
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
searchPlaceholderKey="clients:searchByRoleName"
|
||||
canSelectAll={false}
|
||||
searchTypeComponent={
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
toggleId="role"
|
||||
onToggle={() => setSearchToggle(!searchToggle)}
|
||||
isOpen={searchToggle}
|
||||
variant={isRadio ? SelectVariant.single : SelectVariant.checkbox}
|
||||
hasInlineFilter
|
||||
placeholderText={
|
||||
<>
|
||||
<FilterIcon /> {t("filterByOrigin")}
|
||||
</>
|
||||
}
|
||||
isGrouped
|
||||
onFilter={(evt) => {
|
||||
const value = evt?.target.value || "";
|
||||
return createSelectGroup(
|
||||
clients.filter((client) => client.clientId?.includes(value))
|
||||
);
|
||||
}}
|
||||
selections={selectedClients}
|
||||
onClear={() => setSelectedClients([])}
|
||||
onSelect={(_, selection) => {
|
||||
const client = selection as ClientRole;
|
||||
if (selectedClients.includes(client)) {
|
||||
removeClient(client);
|
||||
} else {
|
||||
setSelectedClients([...selectedClients, client]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{createSelectGroup(clients)}
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
}
|
||||
subToolbar={
|
||||
<ToolbarItem widths={{ default: "100%" }}>
|
||||
<ChipGroup>
|
||||
{selectedClients.map((client) => (
|
||||
<Chip
|
||||
key={`chip-${client.id}`}
|
||||
onClick={() => {
|
||||
removeClient(client);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
{client.clientId || t("realmRoles")}
|
||||
<Badge isRead={true}>{client.numberOfRoles}</Badge>
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</ToolbarItem>
|
||||
}
|
||||
canSelectAll
|
||||
isRadio={isRadio}
|
||||
loader={loader}
|
||||
ariaLabelKey="clients:roles"
|
||||
|
|
|
@ -23,11 +23,38 @@ import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
|||
|
||||
export type CompositeRole = RoleRepresentation & {
|
||||
parent: RoleRepresentation;
|
||||
isInherited?: boolean;
|
||||
};
|
||||
|
||||
export type Row = {
|
||||
client?: ClientRepresentation;
|
||||
role: CompositeRole | RoleRepresentation;
|
||||
role: RoleRepresentation | CompositeRole;
|
||||
};
|
||||
|
||||
export const mapRoles = (
|
||||
assignedRoles: Row[],
|
||||
effectiveRoles: Row[],
|
||||
hide: boolean
|
||||
) => {
|
||||
return [
|
||||
...(hide
|
||||
? assignedRoles.map((row) => ({
|
||||
...row,
|
||||
role: {
|
||||
...row.role,
|
||||
isInherited: false,
|
||||
},
|
||||
}))
|
||||
: effectiveRoles.map((row) => ({
|
||||
...row,
|
||||
role: {
|
||||
...row.role,
|
||||
isInherited:
|
||||
assignedRoles.find((r) => r.role.id === row.role.id) ===
|
||||
undefined,
|
||||
},
|
||||
}))),
|
||||
];
|
||||
};
|
||||
|
||||
export const ServiceRole = ({ role, client }: Row) => (
|
||||
|
@ -153,10 +180,13 @@ export const RoleMapping = ({
|
|||
data-testid="assigned-roles"
|
||||
key={key}
|
||||
loader={loader}
|
||||
canSelectAll={hide}
|
||||
onSelect={hide ? (rows) => setSelected(rows) : undefined}
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelected(rows)}
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
isRowDisabled={(value) =>
|
||||
(value.role as CompositeRole).isInherited || false
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
|
@ -191,6 +221,16 @@ export const RoleMapping = ({
|
|||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("unAssignRole"),
|
||||
onRowClick: async (role) => {
|
||||
setSelected([role]);
|
||||
toggleDeleteDialog();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "role.name",
|
||||
|
|
|
@ -133,6 +133,7 @@ export type DataListProps<T> = {
|
|||
actionResolver?: IActionsResolver;
|
||||
searchTypeComponent?: ReactNode;
|
||||
toolbarItem?: ReactNode;
|
||||
subToolbar?: ReactNode;
|
||||
emptyState?: ReactNode;
|
||||
icon?: React.ComponentClass<SVGIconProps>;
|
||||
isNotCompact?: boolean;
|
||||
|
@ -178,6 +179,7 @@ export function KeycloakDataTable<T>({
|
|||
actionResolver,
|
||||
searchTypeComponent,
|
||||
toolbarItem,
|
||||
subToolbar,
|
||||
emptyState,
|
||||
icon,
|
||||
...props
|
||||
|
@ -388,6 +390,7 @@ export function KeycloakDataTable<T>({
|
|||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
toolbarItem={toolbarItem}
|
||||
subToolbar={subToolbar}
|
||||
>
|
||||
{!loading && data && data.length > 0 && (
|
||||
<DataTable
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { FormEvent, ReactNode } from "react";
|
||||
import {
|
||||
Pagination,
|
||||
ToggleTemplateProps,
|
||||
|
@ -13,14 +13,15 @@ type TableToolbarProps = {
|
|||
onNextClick: (page: number) => void;
|
||||
onPreviousClick: (page: number) => void;
|
||||
onPerPageSelect: (max: number, first: number) => void;
|
||||
searchTypeComponent?: React.ReactNode;
|
||||
toolbarItem?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
searchTypeComponent?: ReactNode;
|
||||
toolbarItem?: ReactNode;
|
||||
subToolbar?: ReactNode;
|
||||
children: ReactNode;
|
||||
inputGroupName?: string;
|
||||
inputGroupPlaceholder?: string;
|
||||
inputGroupOnChange?: (
|
||||
newInput: string,
|
||||
event: React.FormEvent<HTMLInputElement>
|
||||
event: FormEvent<HTMLInputElement>
|
||||
) => void;
|
||||
inputGroupOnEnter?: (value: string) => void;
|
||||
};
|
||||
|
@ -34,6 +35,7 @@ export const PaginatingTableToolbar = ({
|
|||
onPerPageSelect,
|
||||
searchTypeComponent,
|
||||
toolbarItem,
|
||||
subToolbar,
|
||||
children,
|
||||
inputGroupName,
|
||||
inputGroupPlaceholder,
|
||||
|
@ -70,6 +72,7 @@ export const PaginatingTableToolbar = ({
|
|||
)}
|
||||
</>
|
||||
}
|
||||
subToolbar={subToolbar}
|
||||
toolbarItemFooter={
|
||||
count !== 0 ? <ToolbarItem>{pagination("bottom")}</ToolbarItem> : <></>
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FormEvent, Fragment, ReactNode } from "react";
|
||||
import React, { FormEvent, Fragment, ReactNode, useState } from "react";
|
||||
import {
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
|
@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
|
|||
|
||||
type TableToolbarProps = {
|
||||
toolbarItem?: ReactNode;
|
||||
subToolbar?: ReactNode;
|
||||
toolbarItemFooter?: ReactNode;
|
||||
children: ReactNode;
|
||||
searchTypeComponent?: ReactNode;
|
||||
|
@ -28,6 +29,7 @@ type TableToolbarProps = {
|
|||
|
||||
export const TableToolbar = ({
|
||||
toolbarItem,
|
||||
subToolbar,
|
||||
toolbarItemFooter,
|
||||
children,
|
||||
searchTypeComponent,
|
||||
|
@ -37,7 +39,7 @@ export const TableToolbar = ({
|
|||
inputGroupOnEnter,
|
||||
}: TableToolbarProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchValue, setSearchValue] = React.useState<string>("");
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
|
||||
const onSearch = () => {
|
||||
if (searchValue !== "") {
|
||||
|
@ -99,6 +101,11 @@ export const TableToolbar = ({
|
|||
{toolbarItem}
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
{subToolbar && (
|
||||
<Toolbar>
|
||||
<ToolbarContent>{subToolbar}</ToolbarContent>
|
||||
</Toolbar>
|
||||
)}
|
||||
<Divider />
|
||||
{children}
|
||||
<Toolbar>{toolbarItemFooter}</Toolbar>
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
FormattedLink,
|
||||
FormattedLinkProps,
|
||||
} from "../external-link/FormattedLink";
|
||||
import { HelpItem } from "../help-enabler/HelpItem";
|
||||
|
||||
export type ViewHeaderProps = {
|
||||
titleKey: string;
|
||||
|
@ -36,6 +37,7 @@ export type ViewHeaderProps = {
|
|||
isEnabled?: boolean;
|
||||
onToggle?: (value: boolean) => void;
|
||||
divider?: boolean;
|
||||
helpTextKey?: string;
|
||||
};
|
||||
|
||||
export const ViewHeader = ({
|
||||
|
@ -51,6 +53,7 @@ export const ViewHeader = ({
|
|||
isEnabled = true,
|
||||
onToggle,
|
||||
divider = true,
|
||||
helpTextKey,
|
||||
}: ViewHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled } = useContext(HelpContext);
|
||||
|
@ -106,6 +109,13 @@ export const ViewHeader = ({
|
|||
}
|
||||
}}
|
||||
/>
|
||||
{helpTextKey && (
|
||||
<HelpItem
|
||||
helpText={t(helpTextKey)}
|
||||
forLabel={t("common:enabled")}
|
||||
forID={`${titleKey}-switch`}
|
||||
/>
|
||||
)}
|
||||
</ToolbarItem>
|
||||
)}
|
||||
{dropdownItems && (
|
||||
|
|
Loading…
Reference in a new issue