* fixed issues described in #468 fixing: #468 * fixed type * fixed column size and order
This commit is contained in:
parent
83a8f2baa7
commit
4d52871fc2
10 changed files with 405 additions and 88 deletions
71
src/client-scopes/ChangeTypeDialog.tsx
Normal file
71
src/client-scopes/ChangeTypeDialog.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Form,
|
||||
Modal,
|
||||
Radio,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
AllClientScopes,
|
||||
AllClientScopeType,
|
||||
allClientScopeTypes,
|
||||
} from "../components/client-scope/ClientScopeTypes";
|
||||
|
||||
type ChangeTypeDialogProps = {
|
||||
selectedClientScopes: number;
|
||||
onConfirm: (scope: AllClientScopeType) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ChangeTypeDialog = ({
|
||||
selectedClientScopes,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ChangeTypeDialogProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const [value, setValue] = useState<AllClientScopeType>(AllClientScopes.none);
|
||||
return (
|
||||
<Modal
|
||||
title={t("changeType")}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
variant="small"
|
||||
description={t("changeTypeIntro", { count: selectedClientScopes })}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="change-scope-dialog-confirm"
|
||||
key="confirm"
|
||||
onClick={() => onConfirm(value)}
|
||||
>
|
||||
{t("common:continue")}
|
||||
</Button>,
|
||||
<Button
|
||||
key="cancel"
|
||||
variant={ButtonVariant.secondary}
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form isHorizontal>
|
||||
{allClientScopeTypes.map((scope) => (
|
||||
<Radio
|
||||
key={scope}
|
||||
isChecked={scope === value}
|
||||
name={`radio-${scope}`}
|
||||
onChange={(_val, event) => {
|
||||
const { value } = event.currentTarget;
|
||||
setValue(value as AllClientScopeType);
|
||||
}}
|
||||
label={t(`common:clientScope.${scope}`)}
|
||||
id={`radio-${scope}`}
|
||||
value={scope}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,13 +1,80 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { AlertVariant, Button, PageSection } from "@patternfly/react-core";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
KebabToggle,
|
||||
PageSection,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { cellWidth } from "@patternfly/react-table";
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { emptyFormatter } from "../util";
|
||||
import {
|
||||
CellDropdown,
|
||||
ClientScope,
|
||||
AllClientScopes,
|
||||
AllClientScopeType,
|
||||
} from "../components/client-scope/ClientScopeTypes";
|
||||
|
||||
type ClientScopeDefaultOptionalType = ClientScopeRepresentation & {
|
||||
type: AllClientScopeType;
|
||||
};
|
||||
|
||||
import "./client-scope.css";
|
||||
import KeycloakAdminClient from "keycloak-admin";
|
||||
import { ChangeTypeDialog } from "./ChangeTypeDialog";
|
||||
|
||||
const castAdminClient = (adminClient: KeycloakAdminClient) =>
|
||||
(adminClient.clientScopes as unknown) as {
|
||||
[index: string]: Function;
|
||||
};
|
||||
|
||||
const changeScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientScope: ClientScopeDefaultOptionalType,
|
||||
changeTo: AllClientScopeType
|
||||
) => {
|
||||
await removeScope(adminClient, clientScope);
|
||||
await addScope(adminClient, clientScope, changeTo);
|
||||
};
|
||||
|
||||
const removeScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientScope: ClientScopeDefaultOptionalType
|
||||
) => {
|
||||
if (clientScope.type !== AllClientScopes.none)
|
||||
await castAdminClient(adminClient)[
|
||||
`delDefault${
|
||||
clientScope.type === ClientScope.optional ? "Optional" : ""
|
||||
}ClientScope`
|
||||
]({
|
||||
id: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
const addScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientScope: ClientScopeDefaultOptionalType,
|
||||
type: AllClientScopeType
|
||||
) => {
|
||||
if (type !== AllClientScopes.none)
|
||||
await castAdminClient(adminClient)[
|
||||
`addDefault${type === ClientScope.optional ? "Optional" : ""}ClientScope`
|
||||
]({
|
||||
id: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
export const ClientScopesSection = () => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
|
@ -17,7 +84,83 @@ export const ClientScopesSection = () => {
|
|||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const loader = async () => await adminClient.clientScopes.find();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [kebabOpen, setKebabOpen] = useState(false);
|
||||
const [changeTypeOpen, setChangeTypeOpen] = useState(false);
|
||||
const [selectedScopes, setSelectedScopes] = useState<
|
||||
ClientScopeDefaultOptionalType[]
|
||||
>([]);
|
||||
|
||||
const loader = async () => {
|
||||
const defaultScopes = await adminClient.clientScopes.listDefaultClientScopes();
|
||||
const optionalScopes = await adminClient.clientScopes.listDefaultOptionalClientScopes();
|
||||
|
||||
const clientScopes = (await adminClient.clientScopes.find()).map(
|
||||
(scope) => {
|
||||
return {
|
||||
...scope,
|
||||
type: defaultScopes.find(
|
||||
(defaultScope) => defaultScope.name === scope.name
|
||||
)
|
||||
? ClientScope.default
|
||||
: optionalScopes.find(
|
||||
(optionalScope) => optionalScope.name === scope.name
|
||||
)
|
||||
? ClientScope.optional
|
||||
: AllClientScopes.none,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
return clientScopes;
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("deleteClientScope", {
|
||||
count: selectedScopes.length,
|
||||
name: selectedScopes[0]?.name,
|
||||
}),
|
||||
messageKey: "client-scopes:deleteConfirm",
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
for (const scope of selectedScopes) {
|
||||
await adminClient.clientScopes.del({ id: scope.id! });
|
||||
}
|
||||
addAlert(t("deletedSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("deleteError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const TypeSelector = (scope: ClientScopeDefaultOptionalType) => (
|
||||
<>
|
||||
<CellDropdown
|
||||
clientScope={scope}
|
||||
type={scope.type}
|
||||
all
|
||||
onSelect={async (value) => {
|
||||
try {
|
||||
await changeScope(adminClient, scope, value);
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(t("clientScopeError", { error }), AlertVariant.danger);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const ClientScopeDetailLink = (clientScope: ClientScopeRepresentation) => (
|
||||
<>
|
||||
|
@ -28,19 +171,79 @@ export const ClientScopesSection = () => {
|
|||
);
|
||||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
{changeTypeOpen && (
|
||||
<ChangeTypeDialog
|
||||
selectedClientScopes={selectedScopes.length}
|
||||
onConfirm={(type) => {
|
||||
selectedScopes.map(async (scope) => {
|
||||
try {
|
||||
await changeScope(adminClient, scope, type);
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(t("clientScopeError", { error }), AlertVariant.danger);
|
||||
}
|
||||
});
|
||||
setChangeTypeOpen(false);
|
||||
}}
|
||||
onClose={() => setChangeTypeOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<ViewHeader
|
||||
titleKey="clientScopes"
|
||||
subKey="client-scopes:clientScopeExplain"
|
||||
/>
|
||||
<PageSection variant="light" className="pf-u-p-0">
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="client-scopes:clientScopeList"
|
||||
searchPlaceholderKey="client-scopes:searchFor"
|
||||
onSelect={(clientScopes) => setSelectedScopes([...clientScopes])}
|
||||
canSelectAll
|
||||
toolbarItem={
|
||||
<Button onClick={() => history.push(`${url}/new`)}>
|
||||
{t("createClientScope")}
|
||||
</Button>
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => history.push(`${url}/new`)}>
|
||||
{t("createClientScope")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
|
||||
}
|
||||
isOpen={kebabOpen}
|
||||
isPlain
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="changeType"
|
||||
component="button"
|
||||
isDisabled={selectedScopes.length === 0}
|
||||
onClick={() => {
|
||||
setChangeTypeOpen(true);
|
||||
setKebabOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("changeType")}
|
||||
</DropdownItem>,
|
||||
|
||||
<DropdownItem
|
||||
key="action"
|
||||
component="button"
|
||||
isDisabled={selectedScopes.length === 0}
|
||||
onClick={() => {
|
||||
toggleDeleteDialog();
|
||||
setKebabOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("common:delete")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
|
@ -49,20 +252,9 @@ export const ClientScopesSection = () => {
|
|||
},
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: async (clientScope) => {
|
||||
try {
|
||||
await adminClient.clientScopes.del({ id: clientScope.id! });
|
||||
addAlert(t("deletedSuccess"), AlertVariant.success);
|
||||
return true;
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("deleteError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
return false;
|
||||
}
|
||||
onRowClick: (clientScope) => {
|
||||
setSelectedScopes([clientScope]);
|
||||
toggleDeleteDialog();
|
||||
},
|
||||
},
|
||||
]}
|
||||
|
@ -71,11 +263,18 @@ export const ClientScopesSection = () => {
|
|||
name: "name",
|
||||
cellRenderer: ClientScopeDetailLink,
|
||||
},
|
||||
{ name: "description" },
|
||||
{ name: "description", cellFormatters: [emptyFormatter()] },
|
||||
{ name: "type", cellRenderer: TypeSelector },
|
||||
{
|
||||
name: "protocol",
|
||||
displayKey: "client-scopes:protocol",
|
||||
},
|
||||
{
|
||||
name: "attributes['gui.order']",
|
||||
displayKey: "client-scopes:displayOrder",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
transforms: [cellWidth(20)],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</PageSection>
|
||||
|
|
4
src/client-scopes/client-scope.css
Normal file
4
src/client-scopes/client-scope.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
.keycloak__client-scope__none > button .pf-c-select__toggle-text {
|
||||
color: var(--pf-global--Color--400);
|
||||
}
|
|
@ -6,6 +6,14 @@
|
|||
"clientScopeExplain": "Client scopes allow you to define a common set of protocol mappers and roles, which are shared between multiple clients",
|
||||
"searchFor": "Search for client scope",
|
||||
"protocol": "Protocol",
|
||||
"displayOrder": "Display order",
|
||||
"deleteClientScope": "Delete client scope {{name}}",
|
||||
"deleteClientScope_plural": "Delete {{count}} client scopes",
|
||||
"deleteConfirm": "Are you sure you want to delete this client scope",
|
||||
"changeType": "Change type",
|
||||
"changeTypeIntro": "{{count}} selected client scopes will be changed to",
|
||||
"clientScopeSuccess": "Scope mapping updated",
|
||||
"clientScopeError": "Could not update scope mapping {{error}}",
|
||||
"deletedSuccess": "The client scope has been deleted",
|
||||
"deleteError": "Could not delete client scope: {{error}}",
|
||||
"includeInTokenScope": "Include in token scope",
|
||||
|
|
|
@ -30,10 +30,6 @@
|
|||
"evaluate": "Evaluate",
|
||||
"changeTypeTo": "Change type to",
|
||||
"assignRole": "Assign role",
|
||||
"clientScope": {
|
||||
"default": "Default",
|
||||
"optional": "Optional"
|
||||
},
|
||||
"clientScopeSearch": {
|
||||
"client": "Client scope",
|
||||
"assigned": "Assigned type"
|
||||
|
|
|
@ -18,7 +18,10 @@ import {
|
|||
} from "@patternfly/react-table";
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
|
||||
import { ClientScopeType, clientScopeTypesDropdown } from "./ClientScopeTypes";
|
||||
import {
|
||||
ClientScopeType,
|
||||
clientScopeTypesDropdown,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
|
||||
export type AddScopeDialogProps = {
|
||||
clientScopes: ClientScopeRepresentation[];
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react";
|
||||
import { TFunction } from "i18next";
|
||||
import { DropdownItem, SelectOption } from "@patternfly/react-core";
|
||||
|
||||
export enum ClientScope {
|
||||
default = "default",
|
||||
optional = "optional",
|
||||
}
|
||||
export type ClientScopeType = ClientScope.default | ClientScope.optional;
|
||||
const clientScopeTypes = Object.keys(ClientScope);
|
||||
|
||||
export const clientScopeTypesSelectOptions = (t: TFunction) =>
|
||||
clientScopeTypes.map((type) => (
|
||||
<SelectOption key={type} value={type}>
|
||||
{t(`clientScope.${type}`)}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
export const clientScopeTypesDropdown = (
|
||||
t: TFunction,
|
||||
onClick: (scope: ClientScopeType) => void
|
||||
) =>
|
||||
clientScopeTypes.map((type) => (
|
||||
<DropdownItem key={type} onClick={() => onClick(type as ClientScopeType)}>
|
||||
{t(`clientScope.${type}`)}
|
||||
</DropdownItem>
|
||||
));
|
|
@ -22,7 +22,8 @@ import {
|
|||
clientScopeTypesSelectOptions,
|
||||
ClientScopeType,
|
||||
ClientScope,
|
||||
} from "./ClientScopeTypes";
|
||||
CellDropdown,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
|
||||
|
@ -78,41 +79,8 @@ const addScope = async (
|
|||
});
|
||||
};
|
||||
|
||||
type CellDropdownProps = {
|
||||
clientScope: ClientScopeRepresentation;
|
||||
type: ClientScopeType;
|
||||
onSelect: (value: ClientScopeType) => void;
|
||||
};
|
||||
|
||||
const CellDropdown = ({ clientScope, type, onSelect }: CellDropdownProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Select
|
||||
key={clientScope.id}
|
||||
onToggle={() => setOpen(!open)}
|
||||
isOpen={open}
|
||||
selections={[type]}
|
||||
onSelect={(_, value) => {
|
||||
onSelect(value as ClientScopeType);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{clientScopeTypesSelectOptions(t)}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
type SearchType = "client" | "assigned";
|
||||
|
||||
type TableRow = {
|
||||
selected: boolean;
|
||||
clientScope: ClientScopeRepresentation;
|
||||
type: ClientScopeType;
|
||||
cells: (string | undefined)[];
|
||||
};
|
||||
|
||||
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
|
@ -178,7 +146,13 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
type={scope.type}
|
||||
onSelect={async (value) => {
|
||||
try {
|
||||
await changeScope(adminClient, clientId, scope, scope.type, value);
|
||||
await changeScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
scope,
|
||||
scope.type,
|
||||
value as ClientScope
|
||||
);
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
|
|
|
@ -55,6 +55,12 @@
|
|||
"unexpectedError": "An unexpected error occurred: '{{error}}'",
|
||||
"retry": "Retry",
|
||||
|
||||
"clientScope": {
|
||||
"default": "Default",
|
||||
"optional": "Optional",
|
||||
"none": "None"
|
||||
},
|
||||
|
||||
"home": "Home",
|
||||
"manage": "Manage",
|
||||
"clients": "Clients",
|
||||
|
|
83
src/components/client-scope/ClientScopeTypes.tsx
Normal file
83
src/components/client-scope/ClientScopeTypes.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import { TFunction } from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DropdownItem, Select, SelectOption } from "@patternfly/react-core";
|
||||
|
||||
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
|
||||
|
||||
export enum ClientScope {
|
||||
default = "default",
|
||||
optional = "optional",
|
||||
}
|
||||
|
||||
export enum AllClientScopes {
|
||||
none = "none",
|
||||
}
|
||||
|
||||
export type ClientScopeType = ClientScope;
|
||||
export type AllClientScopeType = ClientScope | AllClientScopes;
|
||||
|
||||
const clientScopeTypes = Object.keys(ClientScope);
|
||||
export const allClientScopeTypes = Object.keys({
|
||||
...AllClientScopes,
|
||||
...ClientScope,
|
||||
});
|
||||
|
||||
export const clientScopeTypesSelectOptions = (
|
||||
t: TFunction,
|
||||
scopeTypes: string[] | undefined = clientScopeTypes
|
||||
) =>
|
||||
scopeTypes.map((type) => (
|
||||
<SelectOption key={type} value={type}>
|
||||
{t(`common:clientScope.${type}`)}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
export const clientScopeTypesDropdown = (
|
||||
t: TFunction,
|
||||
onClick: (scope: ClientScopeType) => void
|
||||
) =>
|
||||
clientScopeTypes.map((type) => (
|
||||
<DropdownItem key={type} onClick={() => onClick(type as ClientScopeType)}>
|
||||
{t(`common:clientScope.${type}`)}
|
||||
</DropdownItem>
|
||||
));
|
||||
|
||||
type CellDropdownProps = {
|
||||
clientScope: ClientScopeRepresentation;
|
||||
type: ClientScopeType | AllClientScopeType;
|
||||
all?: boolean;
|
||||
onSelect: (value: ClientScopeType | AllClientScopeType) => void;
|
||||
};
|
||||
|
||||
export const CellDropdown = ({
|
||||
clientScope,
|
||||
type,
|
||||
onSelect,
|
||||
all = false,
|
||||
}: CellDropdownProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Select
|
||||
className={`keycloak__client-scope__${type}`}
|
||||
key={clientScope.id}
|
||||
onToggle={() => setOpen(!open)}
|
||||
isOpen={open}
|
||||
selections={[type]}
|
||||
onSelect={(_, value) => {
|
||||
onSelect(
|
||||
all ? (value as ClientScopeType) : (value as AllClientScopeType)
|
||||
);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{clientScopeTypesSelectOptions(
|
||||
t,
|
||||
all ? allClientScopeTypes : clientScopeTypes
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue