fixed issues described in #468 (#479)

* fixed issues described in #468

fixing: #468

* fixed type

* fixed column size and order
This commit is contained in:
Erik Jan de Wit 2021-04-06 09:29:11 +02:00 committed by GitHub
parent 83a8f2baa7
commit 4d52871fc2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 405 additions and 88 deletions

View 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>
);
};

View file

@ -1,13 +1,80 @@
import React from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Link, useHistory, useRouteMatch } from "react-router-dom"; 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 ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; 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 = () => { export const ClientScopesSection = () => {
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
@ -17,7 +84,83 @@ export const ClientScopesSection = () => {
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert } = useAlerts(); 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) => ( const ClientScopeDetailLink = (clientScope: ClientScopeRepresentation) => (
<> <>
@ -28,19 +171,79 @@ export const ClientScopesSection = () => {
); );
return ( 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 <ViewHeader
titleKey="clientScopes" titleKey="clientScopes"
subKey="client-scopes:clientScopeExplain" subKey="client-scopes:clientScopeExplain"
/> />
<PageSection variant="light" className="pf-u-p-0"> <PageSection variant="light" className="pf-u-p-0">
<KeycloakDataTable <KeycloakDataTable
key={key}
loader={loader} loader={loader}
ariaLabelKey="client-scopes:clientScopeList" ariaLabelKey="client-scopes:clientScopeList"
searchPlaceholderKey="client-scopes:searchFor" searchPlaceholderKey="client-scopes:searchFor"
onSelect={(clientScopes) => setSelectedScopes([...clientScopes])}
canSelectAll
toolbarItem={ toolbarItem={
<>
<ToolbarItem>
<Button onClick={() => history.push(`${url}/new`)}> <Button onClick={() => history.push(`${url}/new`)}>
{t("createClientScope")} {t("createClientScope")}
</Button> </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={[ actions={[
{ {
@ -49,20 +252,9 @@ export const ClientScopesSection = () => {
}, },
{ {
title: t("common:delete"), title: t("common:delete"),
onRowClick: async (clientScope) => { onRowClick: (clientScope) => {
try { setSelectedScopes([clientScope]);
await adminClient.clientScopes.del({ id: clientScope.id! }); toggleDeleteDialog();
addAlert(t("deletedSuccess"), AlertVariant.success);
return true;
} catch (error) {
addAlert(
t("deleteError", {
error: error.response?.data?.errorMessage || error,
}),
AlertVariant.danger
);
return false;
}
}, },
}, },
]} ]}
@ -71,11 +263,18 @@ export const ClientScopesSection = () => {
name: "name", name: "name",
cellRenderer: ClientScopeDetailLink, cellRenderer: ClientScopeDetailLink,
}, },
{ name: "description" }, { name: "description", cellFormatters: [emptyFormatter()] },
{ name: "type", cellRenderer: TypeSelector },
{ {
name: "protocol", name: "protocol",
displayKey: "client-scopes:protocol", displayKey: "client-scopes:protocol",
}, },
{
name: "attributes['gui.order']",
displayKey: "client-scopes:displayOrder",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(20)],
},
]} ]}
/> />
</PageSection> </PageSection>

View file

@ -0,0 +1,4 @@
.keycloak__client-scope__none > button .pf-c-select__toggle-text {
color: var(--pf-global--Color--400);
}

View file

@ -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", "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", "searchFor": "Search for client scope",
"protocol": "Protocol", "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", "deletedSuccess": "The client scope has been deleted",
"deleteError": "Could not delete client scope: {{error}}", "deleteError": "Could not delete client scope: {{error}}",
"includeInTokenScope": "Include in token scope", "includeInTokenScope": "Include in token scope",

View file

@ -30,10 +30,6 @@
"evaluate": "Evaluate", "evaluate": "Evaluate",
"changeTypeTo": "Change type to", "changeTypeTo": "Change type to",
"assignRole": "Assign role", "assignRole": "Assign role",
"clientScope": {
"default": "Default",
"optional": "Optional"
},
"clientScopeSearch": { "clientScopeSearch": {
"client": "Client scope", "client": "Client scope",
"assigned": "Assigned type" "assigned": "Assigned type"

View file

@ -18,7 +18,10 @@ import {
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import { ClientScopeType, clientScopeTypesDropdown } from "./ClientScopeTypes"; import {
ClientScopeType,
clientScopeTypesDropdown,
} from "../../components/client-scope/ClientScopeTypes";
export type AddScopeDialogProps = { export type AddScopeDialogProps = {
clientScopes: ClientScopeRepresentation[]; clientScopes: ClientScopeRepresentation[];

View file

@ -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>
));

View file

@ -22,7 +22,8 @@ import {
clientScopeTypesSelectOptions, clientScopeTypesSelectOptions,
ClientScopeType, ClientScopeType,
ClientScope, ClientScope,
} from "./ClientScopeTypes"; CellDropdown,
} from "../../components/client-scope/ClientScopeTypes";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; 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 SearchType = "client" | "assigned";
type TableRow = {
selected: boolean;
clientScope: ClientScopeRepresentation;
type: ClientScopeType;
cells: (string | undefined)[];
};
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -178,7 +146,13 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
type={scope.type} type={scope.type}
onSelect={async (value) => { onSelect={async (value) => {
try { try {
await changeScope(adminClient, clientId, scope, scope.type, value); await changeScope(
adminClient,
clientId,
scope,
scope.type,
value as ClientScope
);
addAlert(t("clientScopeSuccess"), AlertVariant.success); addAlert(t("clientScopeSuccess"), AlertVariant.success);
refresh(); refresh();
} catch (error) { } catch (error) {

View file

@ -55,6 +55,12 @@
"unexpectedError": "An unexpected error occurred: '{{error}}'", "unexpectedError": "An unexpected error occurred: '{{error}}'",
"retry": "Retry", "retry": "Retry",
"clientScope": {
"default": "Default",
"optional": "Optional",
"none": "None"
},
"home": "Home", "home": "Home",
"manage": "Manage", "manage": "Manage",
"clients": "Clients", "clients": "Clients",

View 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>
);
};