initial version of the scopes screen (#1750)

* initial version of the scopes screen

* added scopes edit details

* Update src/clients/authorization/MoreLabel.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* pr review

* Update src/clients/routes/NewScope.ts

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/clients/authorization/Scopes.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/clients/authorization/Scopes.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* merge fix

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2022-01-04 10:17:43 +01:00 committed by GitHub
parent f0fc136672
commit e6d8ffb202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 686 additions and 39 deletions

30
package-lock.json generated
View file

@ -7,7 +7,7 @@
"name": "keycloak-admin-ui",
"license": "Apache",
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.1.0",
"@keycloak/keycloak-admin-client": "^17.0.0-dev.5",
"@patternfly/patternfly": "^4.164.2",
"@patternfly/react-code-editor": "^4.22.1",
"@patternfly/react-core": "^4.181.1",
@ -3413,13 +3413,13 @@
}
},
"node_modules/@keycloak/keycloak-admin-client": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.1.0.tgz",
"integrity": "sha512-QEibP/Jap+cwU/xB79eQQojBnNdBrWiatr98ARtKZSpyIOh0XYe4FB6YzsgGYj343KygSDLqjhAZ9nurHx64Rw==",
"version": "17.0.0-dev.5",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-17.0.0-dev.5.tgz",
"integrity": "sha512-WR+5eBunhyDMAErMqu3cUT1cSOZEhb8ie4QuIBNlZASeffXQQJdlosrA8kOkxUFo+SEYycuatKE+fkAD3+hFjw==",
"dependencies": {
"axios": "^0.24.0",
"camelize-ts": "^1.0.8",
"keycloak-js": "^15.0.2",
"keycloak-js": "^16.0.0",
"lodash": "^4.17.21",
"query-string": "^7.0.1",
"url-join": "^4.0.0",
@ -14204,9 +14204,9 @@
"dev": true
},
"node_modules/keycloak-js": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-15.0.2.tgz",
"integrity": "sha512-dv2a4NcPSH3AzGWG3ZtB+VrHpuQLdFBYXtQBj/+oBzm6XNwnVAMdL6LIC0OzCLQpn3rKTQJtNSATAGhbKJgewQ==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-16.1.0.tgz",
"integrity": "sha512-ydD0SJ+cLmtlor5MvyIOJygnGHueWwnAtXvqniv19k4TslcSpAEACTsnsvENdKa7/NTC4/erg6NctS4uF3nMdw==",
"dependencies": {
"base64-js": "1.3.1",
"js-sha256": "0.9.0"
@ -23870,13 +23870,13 @@
}
},
"@keycloak/keycloak-admin-client": {
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.1.0.tgz",
"integrity": "sha512-QEibP/Jap+cwU/xB79eQQojBnNdBrWiatr98ARtKZSpyIOh0XYe4FB6YzsgGYj343KygSDLqjhAZ9nurHx64Rw==",
"version": "17.0.0-dev.5",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-17.0.0-dev.5.tgz",
"integrity": "sha512-WR+5eBunhyDMAErMqu3cUT1cSOZEhb8ie4QuIBNlZASeffXQQJdlosrA8kOkxUFo+SEYycuatKE+fkAD3+hFjw==",
"requires": {
"axios": "^0.24.0",
"camelize-ts": "^1.0.8",
"keycloak-js": "^15.0.2",
"keycloak-js": "^16.0.0",
"lodash": "^4.17.21",
"query-string": "^7.0.1",
"url-join": "^4.0.0",
@ -32374,9 +32374,9 @@
"dev": true
},
"keycloak-js": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-15.0.2.tgz",
"integrity": "sha512-dv2a4NcPSH3AzGWG3ZtB+VrHpuQLdFBYXtQBj/+oBzm6XNwnVAMdL6LIC0OzCLQpn3rKTQJtNSATAGhbKJgewQ==",
"version": "16.1.0",
"resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-16.1.0.tgz",
"integrity": "sha512-ydD0SJ+cLmtlor5MvyIOJygnGHueWwnAtXvqniv19k4TslcSpAEACTsnsvENdKa7/NTC4/erg6NctS4uF3nMdw==",
"requires": {
"base64-js": "1.3.1",
"js-sha256": "0.9.0"

View file

@ -23,7 +23,7 @@
"prepare": "husky install"
},
"dependencies": {
"@keycloak/keycloak-admin-client": "^16.1.0",
"@keycloak/keycloak-admin-client": "^17.0.0-dev.5",
"@patternfly/patternfly": "^4.164.2",
"@patternfly/react-code-editor": "^4.22.1",
"@patternfly/react-core": "^4.181.1",

View file

@ -57,6 +57,7 @@ import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/l
import { toMapper } from "./routes/Mapper";
import { AuthorizationSettings } from "./authorization/Settings";
import { AuthorizationResources } from "./authorization/Resources";
import { AuthorizationScopes } from "./authorization/Scopes";
type ClientDetailHeaderProps = {
onChange: (value: boolean) => void;
@ -484,6 +485,13 @@ export default function ClientDetails() {
>
<AuthorizationResources clientId={clientId} />
</Tab>
<Tab
id="scopes"
eventKey={42}
title={<TabTitleText>{t("scopes")}</TabTitleText>}
>
<AuthorizationScopes clientId={clientId} />
</Tab>
</Tabs>
</Tab>
)}

View file

@ -0,0 +1,75 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Alert, AlertVariant } from "@patternfly/react-core";
import type { ExpandableScopeRepresentation } from "./Scopes";
import { useAlerts } from "../../components/alert/Alerts";
import { ConfirmDialogModal } from "../../components/confirm-dialog/ConfirmDialog";
import { useAdminClient } from "../../context/auth/AdminClient";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
type DeleteScopeDialogProps = {
clientId: string;
selectedScope:
| ExpandableScopeRepresentation
| ScopeRepresentation
| undefined;
refresh: () => void;
open: boolean;
toggleDialog: () => void;
};
export const DeleteScopeDialog = ({
clientId,
selectedScope,
refresh,
open,
toggleDialog,
}: DeleteScopeDialogProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
return (
<ConfirmDialogModal
open={open}
toggleDialog={toggleDialog}
titleKey="clients:deleteScope"
continueButtonLabel="clients:confirm"
onConfirm={async () => {
try {
await adminClient.clients.delAuthorizationScope({
id: clientId,
scopeId: selectedScope?.id!,
});
addAlert(t("resourceScopeSuccess"), AlertVariant.success);
refresh();
} catch (error) {
addError("clients:resourceScopeError", error);
}
}}
>
{t("deleteScopeConfirm")}
{selectedScope &&
"permissions" in selectedScope &&
selectedScope.permissions &&
selectedScope.permissions.length > 0 && (
<Alert
variant="warning"
isInline
isPlain
title={t("deleteScopeWarning")}
className="pf-u-pt-lg"
>
<p className="pf-u-pt-xs">
{selectedScope.permissions.map((permission) => (
<strong key={permission.id} className="pf-u-pr-md">
{permission.name}
</strong>
))}
</p>
</Alert>
)}
</ConfirmDialogModal>
);
};

View file

@ -0,0 +1,18 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Label } from "@patternfly/react-core";
type MoreLabelProps = {
array: unknown[] | undefined;
};
export const MoreLabel = ({ array }: MoreLabelProps) => {
const { t } = useTranslation("clients");
if (!array || array.length <= 1) {
return null;
}
return (
<Label color="blue">{t("common:more", { count: array.length - 1 })}</Label>
);
};

View file

@ -37,12 +37,6 @@ import { AttributeInput } from "../../components/attribute-input/AttributeInput"
import "./resource-details.css";
type FetchResource = {
client?: ClientRepresentation;
resource?: ResourceRepresentation;
permissions?: ResourceServerRepresentation[];
};
type SubmittedResource = Omit<ResourceRepresentation, "attributes" | "uris"> & {
attributes: KeyValueType[];
uris: MultiLine[];
@ -72,8 +66,8 @@ export default function ResourceDetails() {
};
useFetch(
async (): Promise<FetchResource> => {
const [client, resource, permissions] = await Promise.all([
() =>
Promise.all([
adminClient.clients.findOne({ id }),
resourceId
? adminClient.clients.getResource({ id, resourceId })
@ -81,11 +75,8 @@ export default function ResourceDetails() {
resourceId
? adminClient.clients.listPermissionsByResource({ id, resourceId })
: Promise.resolve(undefined),
]);
return { client, resource, permissions };
},
({ client, resource, permissions }) => {
]),
([client, resource, permissions]) => {
if (!client) {
throw new Error(t("common:notFound"));
}

View file

@ -5,7 +5,6 @@ import {
Alert,
AlertVariant,
Button,
Label,
PageSection,
ToolbarItem,
} from "@patternfly/react-core";
@ -30,6 +29,7 @@ import { DetailCell } from "./DetailCell";
import { toCreateResource } from "../routes/NewResource";
import { useRealm } from "../../context/realm-context/RealmContext";
import { toResourceDetails } from "../routes/Resource";
import { MoreLabel } from "./MoreLabel";
type ResourcesProps = {
clientId: string;
@ -79,12 +79,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
const UriRenderer = ({ row }: { row: ResourceRepresentation }) => (
<>
{row.uris?.[0]}{" "}
{(row.uris?.length || 0) > 1 && (
<Label color="blue">
{t("common:more", { count: (row.uris?.length || 1) - 1 })}
</Label>
)}
{row.uris?.[0]} <MoreLabel array={row.uris} />
</>
);
@ -229,7 +224,7 @@ export const AuthorizationResources = ({ clientId }: ResourcesProps) => {
},
],
}}
></Td>
/>
</Tr>
<Tr
key={`child-${resource._id}`}

View file

@ -0,0 +1,211 @@
import React, { useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
DropdownItem,
FormGroup,
PageSection,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import type { ScopeDetailsParams } from "../routes/Scope";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { toClient } from "../routes/Client";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useAlerts } from "../../components/alert/Alerts";
import useToggle from "../../utils/useToggle";
import { DeleteScopeDialog } from "./DeleteScopeDialog";
export default function ScopeDetails() {
const { t } = useTranslation("clients");
const { id, scopeId, realm } = useParams<ScopeDetailsParams>();
const history = useHistory();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const [deleteDialog, toggleDeleteDialog] = useToggle();
const [scope, setScope] = useState<ScopeRepresentation>();
const { register, errors, reset, handleSubmit } =
useForm<ScopeRepresentation>({
mode: "onChange",
});
useFetch(
async () => {
if (scopeId) {
const scope = await adminClient.clients.getAuthorizationScope({
id,
scopeId,
});
if (!scope) {
throw new Error(t("common:notFound"));
}
return scope;
}
},
(scope) => {
setScope(scope);
reset({ ...scope });
},
[]
);
const save = async (scope: ScopeRepresentation) => {
try {
if (scopeId) {
await adminClient.clients.updateAuthorizationScope(
{ id, scopeId },
scope
);
setScope(scope);
} else {
await adminClient.clients.createAuthorizationScope(
{ id },
{
name: scope.name!,
displayName: scope.displayName,
iconUri: scope.iconUri,
}
);
history.push(toClient({ realm, clientId: id, tab: "authorization" }));
}
addAlert(
t((scopeId ? "update" : "create") + "ScopeSuccess"),
AlertVariant.success
);
} catch (error) {
addError("clients:scopeSaveError", error);
}
};
return (
<>
<DeleteScopeDialog
clientId={id}
open={deleteDialog}
toggleDialog={toggleDeleteDialog}
selectedScope={scope}
refresh={() =>
history.push(toClient({ realm, clientId: id, tab: "authorization" }))
}
/>
<ViewHeader
titleKey={"clients:createResource"}
dropdownItems={
scopeId
? [
<DropdownItem
key="delete"
data-testid="delete-resource"
onClick={() => toggleDeleteDialog()}
>
{t("common:delete")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
<FormAccess
isHorizontal
role="manage-clients"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("common:name")}
fieldId="name"
labelIcon={
<HelpItem helpText="clients-help:scopeName" fieldLabelId="name" />
}
helperTextInvalid={t("common:required")}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
isRequired
>
<TextInput
id="name"
name="name"
ref={register({ required: true })}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("displayName")}
fieldId="displayName"
labelIcon={
<HelpItem
helpText="clients-help:scopeDisplayName"
fieldLabelId="displayName"
/>
}
>
<TextInput id="displayName" name="displayName" ref={register} />
</FormGroup>
<FormGroup
label={t("iconUri")}
fieldId="iconUri"
labelIcon={
<HelpItem
helpText="clients-help:iconUri"
fieldLabelId="clients:iconUri"
/>
}
>
<TextInput id="iconUri" name="iconUri" ref={register} />
</FormGroup>
<ActionGroup>
<div className="pf-u-mt-md">
<Button
variant={ButtonVariant.primary}
type="submit"
data-testid="save"
>
{t("common:save")}
</Button>
{!scope ? (
<Button
variant="link"
data-testid="cancel"
component={(props) => (
<Link
{...props}
to={toClient({
realm,
clientId: id,
tab: "authorization",
})}
></Link>
)}
>
{t("common:cancel")}
</Button>
) : (
<Button
variant="link"
data-testid="revert"
onClick={() => reset({ ...scope })}
>
{t("common:revert")}
</Button>
)}
</div>
</ActionGroup>
</FormAccess>
</PageSection>
</>
);
}

View file

@ -0,0 +1,283 @@
import React, { useState } from "react";
import { Link, useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
Button,
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
PageSection,
ToolbarItem,
} from "@patternfly/react-core";
import {
ExpandableRowContent,
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
import type PolicyRepresentation from "@keycloak/keycloak-admin-client/lib/defs/policyRepresentation";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { PaginatingTableToolbar } from "../../components/table-toolbar/PaginatingTableToolbar";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { MoreLabel } from "./MoreLabel";
import { toScopeDetails } from "../routes/Scope";
import { toNewScope } from "../routes/NewScope";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import useToggle from "../../utils/useToggle";
import { DeleteScopeDialog } from "./DeleteScopeDialog";
type ScopesProps = {
clientId: string;
};
export type ExpandableScopeRepresentation = ScopeRepresentation & {
permissions?: PolicyRepresentation[];
isExpanded: boolean;
};
export const AuthorizationScopes = ({ clientId }: ScopesProps) => {
const { t } = useTranslation("clients");
const history = useHistory();
const adminClient = useAdminClient();
const { realm } = useRealm();
const [deleteDialog, toggleDeleteDialog] = useToggle();
const [scopes, setScopes] = useState<ExpandableScopeRepresentation[]>();
const [selectedScope, setSelectedScope] =
useState<ExpandableScopeRepresentation>();
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
const [max, setMax] = useState(10);
const [first, setFirst] = useState(0);
useFetch(
async () => {
const params = {
first,
max,
deep: false,
};
const scopes = await adminClient.clients.listAllScopes({
...params,
id: clientId,
});
return await Promise.all(
scopes.map(async (scope) => {
const options = { id: clientId, scopeId: scope.id! };
const [resources, permissions] = await Promise.all([
adminClient.clients.listAllResourcesByScope(options),
adminClient.clients.listAllPermissionsByScope(options),
]);
return {
...scope,
resources,
permissions,
isExpanded: false,
};
})
);
},
setScopes,
[key]
);
const ResourceRenderer = ({
row,
}: {
row: ExpandableScopeRepresentation;
}) => {
return (
<>
{row.resources?.[0]?.name} <MoreLabel array={row.resources} />
</>
);
};
const PermissionsRenderer = ({
row,
}: {
row: ExpandableScopeRepresentation;
}) => {
return (
<>
{row.permissions?.[0]?.name} <MoreLabel array={row.permissions} />
</>
);
};
if (!scopes) {
return <KeycloakSpinner />;
}
return (
<PageSection variant="light" className="pf-u-p-0">
<DeleteScopeDialog
clientId={clientId}
open={deleteDialog}
toggleDialog={toggleDeleteDialog}
selectedScope={selectedScope}
refresh={refresh}
/>
{scopes.length > 0 && (
<PaginatingTableToolbar
count={scopes.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(first, max) => {
setFirst(first);
setMax(max);
}}
toolbarItem={
<ToolbarItem>
<Button
data-testid="createAuthorizationScope"
component={(props) => (
<Link {...props} to={toNewScope({ realm, id: clientId })} />
)}
>
{t("createAuthorizationScope")}
</Button>
</ToolbarItem>
}
>
<TableComposable aria-label={t("scopes")} variant="compact">
<Thead>
<Tr>
<Th />
<Th>{t("common:name")}</Th>
<Th>{t("resources")}</Th>
<Th>{t("permissions")}</Th>
<Th />
</Tr>
</Thead>
{scopes.map((scope, rowIndex) => (
<Tbody key={scope.id} isExpanded={scope.isExpanded}>
<Tr>
<Td
expand={{
rowIndex,
isExpanded: scope.isExpanded,
onToggle: (_, rowIndex) => {
const rows = scopes.map((resource, index) =>
index === rowIndex
? { ...resource, isExpanded: !resource.isExpanded }
: resource
);
setScopes(rows);
},
}}
/>
<Td data-testid={`name-column-${scope.name}`}>
<Link
to={toScopeDetails({
realm,
id: clientId,
scopeId: scope.id!,
})}
>
{scope.name}
</Link>
</Td>
<Td>
<ResourceRenderer row={scope} />
</Td>
<Td>
<PermissionsRenderer row={scope} />
</Td>
<Td
actions={{
items: [
{
title: t("common:delete"),
onClick: () => {
setSelectedScope(scope);
toggleDeleteDialog();
},
},
{
title: t("createPermission"),
className: "pf-m-link",
isOutsideDropdown: true,
},
],
}}
/>
</Tr>
<Tr key={`child-${scope.id}`} isExpanded={scope.isExpanded}>
<Td colSpan={5}>
<ExpandableRowContent>
{scope.isExpanded && (
<DescriptionList
isHorizontal
className="keycloak_resource_details"
>
<DescriptionListGroup>
<DescriptionListTerm>
{t("resources")}
</DescriptionListTerm>
<DescriptionListDescription>
{scope.resources?.map((resource) => (
<span key={resource._id} className="pf-u-pr-sm">
{resource.name}
</span>
))}
{scope.resources?.length === 0 && (
<i>{t("common:none")}</i>
)}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>
{t("associatedPermissions")}
</DescriptionListTerm>
<DescriptionListDescription>
{scope.permissions?.map((permission) => (
<span
key={permission.id}
className="pf-u-pr-sm"
>
{permission.name}
</span>
))}
{scope.permissions?.length === 0 && (
<i>{t("common:none")}</i>
)}
</DescriptionListDescription>
</DescriptionListGroup>
</DescriptionList>
)}
</ExpandableRowContent>
</Td>
</Tr>
</Tbody>
))}
</TableComposable>
</PaginatingTableToolbar>
)}
{scopes.length === 0 && (
<ListEmptyState
message={t("emptyAuthorizationScopes")}
instructions={t("emptyAuthorizationInstructions")}
onPrimaryAction={() =>
history.push(toNewScope({ id: clientId, realm }))
}
primaryActionText={t("createAuthorizationScope")}
/>
)}
</PageSection>
);
};

View file

@ -176,5 +176,9 @@ export default {
resetActions:
"Set of actions to execute when sending the user a Reset Actions Email. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.",
lifespan: "Maximum time before the action permit expires.",
scopeName:
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
scopeDisplayName:
"A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.",
},
};

View file

@ -93,6 +93,22 @@ export default {
"The permissions below will be removed when they are no longer used by other resources:",
resourceDeletedSuccess: "The resource successfully deleted",
resourceDeletedError: "Could not remove the resource {{error}}",
deleteScope: "Permanently delete authorization scope?",
deleteScopeConfirm:
"If you delete this authorization scope, some permissions will be affected.",
deleteScopeWarning:
"The permissions below will be removed when they are no longer used by other authorization scopes:",
resourceScopeSuccess: "The authorization scope successfully deleted",
resourceScopeError:
"Could not remove the authorization scope due to {{error}}",
createAuthorizationScope: "Create authorization scope",
permissions: "Permissions",
emptyAuthorizationScopes: "No authorization scopes",
emptyAuthorizationInstructions:
"If you want to create authorization scopes, please click the button below to create the authorization scope",
createScopeSuccess: "Authorization scope created successfully",
updateScopeSuccess: "Authorization scope successfully updated",
scopeSaveError: "Could not persist authorization scope due to {{error}}",
assignedClientScope: "Assigned client scope",
assignedType: "Assigned type",
hideInheritedRoles: "Hide inherited roles",

View file

@ -7,6 +7,8 @@ import { ImportClientRoute } from "./routes/ImportClient";
import { MapperRoute } from "./routes/Mapper";
import { NewResourceRoute } from "./routes/NewResource";
import { ResourceDetailsRoute } from "./routes/Resource";
import { NewScopeRoute } from "./routes/NewScope";
import { ScopeDetailsRoute } from "./routes/Scope";
const routes: RouteDef[] = [
AddClientRoute,
@ -17,6 +19,8 @@ const routes: RouteDef[] = [
MapperRoute,
NewResourceRoute,
ResourceDetailsRoute,
NewScopeRoute,
ScopeDetailsRoute,
];
export default routes;

View file

@ -6,7 +6,7 @@ import { lazy } from "react";
export type NewResourceParams = { realm: string; id: string };
export const NewResourceRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/new",
path: "/:realm/clients/:id/authorization/resource/new",
component: lazy(() => import("../authorization/ResourceDetails")),
breadcrumb: (t) => t("clients:createResource"),
access: "manage-clients",

View file

@ -0,0 +1,19 @@
import type { LocationDescriptorObject } from "history";
import type { RouteDef } from "../../route-config";
import { generatePath } from "react-router-dom";
import { lazy } from "react";
export type NewScopeParams = { realm: string; id: string };
export const NewScopeRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/scope/new",
component: lazy(() => import("../authorization/ScopeDetails")),
breadcrumb: (t) => t("clients:createAuthorizationScope"),
access: "manage-clients",
};
export const toNewScope = (
params: NewScopeParams
): LocationDescriptorObject => ({
pathname: generatePath(NewScopeRoute.path, params),
});

View file

@ -10,7 +10,7 @@ export type ResourceDetailsParams = {
};
export const ResourceDetailsRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/:resourceId?",
path: "/:realm/clients/:id/authorization/resource/:resourceId?",
component: lazy(() => import("../authorization/ResourceDetails")),
breadcrumb: (t) => t("clients:createResource"),
access: "manage-clients",

View file

@ -0,0 +1,23 @@
import type { LocationDescriptorObject } from "history";
import type { RouteDef } from "../../route-config";
import { generatePath } from "react-router-dom";
import { lazy } from "react";
export type ScopeDetailsParams = {
realm: string;
id: string;
scopeId?: string;
};
export const ScopeDetailsRoute: RouteDef = {
path: "/:realm/clients/:id/authorization/scope/:scopeId?",
component: lazy(() => import("../authorization/ScopeDetails")),
breadcrumb: (t) => t("clients:createAuthorizationScope"),
access: "manage-clients",
};
export const toScopeDetails = (
params: ScopeDetailsParams
): LocationDescriptorObject => ({
pathname: generatePath(ScopeDetailsRoute.path, params),
});