Fixing UXD design review (#560)

* Fixing UXD design review

* Removed divider
* Adding type field
* Hide "Consent screen text" field when display consent screen is off
* Fixed cancel button

* fixed type save

* type fix
This commit is contained in:
Erik Jan de Wit 2021-05-21 18:34:05 +02:00 committed by GitHub
parent 9c82353f37
commit 0f6ce35687
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 191 additions and 78 deletions

View file

@ -12,7 +12,6 @@ import {
ToolbarItem, ToolbarItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { cellWidth } from "@patternfly/react-table"; import { cellWidth } from "@patternfly/react-table";
import type 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";
@ -24,58 +23,14 @@ import {
CellDropdown, CellDropdown,
ClientScope, ClientScope,
AllClientScopes, AllClientScopes,
AllClientScopeType, ClientScopeDefaultOptionalType,
changeScope,
removeScope,
} from "../components/client-scope/ClientScopeTypes"; } from "../components/client-scope/ClientScopeTypes";
import type KeycloakAdminClient from "keycloak-admin";
import { ChangeTypeDialog } from "./ChangeTypeDialog"; import { ChangeTypeDialog } from "./ChangeTypeDialog";
import "./client-scope.css"; import "./client-scope.css";
type ClientScopeDefaultOptionalType = ClientScopeRepresentation & {
type: AllClientScopeType;
};
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");
const history = useHistory(); const history = useHistory();
@ -128,6 +83,7 @@ export const ClientScopesSection = () => {
onConfirm: async () => { onConfirm: async () => {
try { try {
for (const scope of selectedScopes) { for (const scope of selectedScopes) {
await removeScope(adminClient, scope);
await adminClient.clientScopes.del({ id: scope.id! }); await adminClient.clientScopes.del({ id: scope.id! });
} }
addAlert(t("deletedSuccess"), AlertVariant.success); addAlert(t("deletedSuccess"), AlertVariant.success);
@ -162,9 +118,14 @@ export const ClientScopesSection = () => {
</> </>
); );
const ClientScopeDetailLink = (clientScope: ClientScopeRepresentation) => ( const ClientScopeDetailLink = (
clientScope: ClientScopeDefaultOptionalType
) => (
<> <>
<Link key={clientScope.id} to={`${url}/${clientScope.id}/settings`}> <Link
key={clientScope.id}
to={`${url}/${clientScope.id}/${clientScope.type}/settings`}
>
{clientScope.name} {clientScope.name}
</Link> </Link>
</> </>

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom"; import { useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
import { import {
Form, Form,
FormGroup, FormGroup,
@ -16,23 +16,37 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import type ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import {
clientScopeTypesSelectOptions,
allClientScopeTypes,
ClientScopeDefaultOptionalType,
} from "../../components/client-scope/ClientScopeTypes";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider"; import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { convertToFormValues } from "../../util"; import { convertToFormValues } from "../../util";
import { useRealm } from "../../context/realm-context/RealmContext";
type ScopeFormProps = { type ScopeFormProps = {
clientScope: ClientScopeRepresentation; clientScope: ClientScopeRepresentation;
save: (clientScope: ClientScopeRepresentation) => void; save: (clientScope: ClientScopeDefaultOptionalType) => void;
}; };
export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => { export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
const { register, control, handleSubmit, errors, setValue } = useForm(); const { register, control, handleSubmit, errors, setValue } = useForm();
const history = useHistory(); const history = useHistory();
const { realm } = useRealm();
const providers = useLoginProviders(); const providers = useLoginProviders();
const [open, isOpen] = useState(false); const [open, isOpen] = useState(false);
const [openType, setOpenType] = useState(false);
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const displayOnConsentScreen = useWatch({
control,
name: "attributes.display-on-consent-screen",
});
useEffect(() => { useEffect(() => {
Object.entries(clientScope).map((entry) => { Object.entries(clientScope).map((entry) => {
if (entry[0] === "attributes") { if (entry[0] === "attributes") {
@ -99,6 +113,38 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
name="description" name="description"
/> />
</FormGroup> </FormGroup>
<FormGroup
label={t("type")}
labelIcon={
<HelpItem
helpText="client-scopes-help:type"
forLabel={t("type")}
forID="type"
/>
}
fieldId="type"
>
<Controller
name="type"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
id="type"
variant={SelectVariant.single}
isOpen={openType}
selections={value}
onToggle={() => setOpenType(!openType)}
onSelect={(_, value) => {
onChange(value);
setOpenType(false);
}}
>
{clientScopeTypesSelectOptions(t, allClientScopeTypes)}
</Select>
)}
/>
</FormGroup>
{!id && ( {!id && (
<FormGroup <FormGroup
label={t("protocol")} label={t("protocol")}
@ -168,6 +214,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
)} )}
/> />
</FormGroup> </FormGroup>
{displayOnConsentScreen === "true" && (
<FormGroup <FormGroup
label={t("consentScreenText")} label={t("consentScreenText")}
labelIcon={ labelIcon={
@ -186,6 +233,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
name="attributes.consent-screen-text" name="attributes.consent-screen-text"
/> />
</FormGroup> </FormGroup>
)}
<FormGroup <FormGroup
hasNoPaddingTop hasNoPaddingTop
label={t("includeInTokenScope")} label={t("includeInTokenScope")}
@ -246,7 +294,10 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
{t("common:save")} {t("common:save")}
</Button> </Button>
<Button variant="link" onClick={() => history.push("/client-scopes/")}> <Button
variant="link"
onClick={() => history.push(`/${realm}/client-scopes`)}
>
{t("common:cancel")} {t("common:cancel")}
</Button> </Button>
</ActionGroup> </ActionGroup>

View file

@ -3,13 +3,14 @@ import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
AlertVariant, AlertVariant,
ButtonVariant,
DropdownItem,
PageSection, PageSection,
Spinner, Spinner,
Tab, Tab,
TabTitleText, TabTitleText,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { KeycloakTabs } from "../../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../../components/keycloak-tabs/KeycloakTabs";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
@ -17,16 +18,24 @@ import { ViewHeader } from "../../components/view-header/ViewHeader";
import { convertFormValuesToObject } from "../../util"; import { convertFormValuesToObject } from "../../util";
import { MapperList } from "../details/MapperList"; import { MapperList } from "../details/MapperList";
import { ScopeForm } from "../details/ScopeForm"; import { ScopeForm } from "../details/ScopeForm";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { RoleMapping, Row } from "../../components/role-mapping/RoleMapping"; import { RoleMapping, Row } from "../../components/role-mapping/RoleMapping";
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation"; import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
import {
AllClientScopes,
changeScope,
ClientScopeDefaultOptionalType,
} from "../../components/client-scope/ClientScopeTypes";
export const ClientScopeForm = () => { export const ClientScopeForm = () => {
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
const [clientScope, setClientScope] = useState<ClientScopeRepresentation>(); const [clientScope, setClientScope] = useState<
ClientScopeDefaultOptionalType
>();
const [hide, setHide] = useState(false); const [hide, setHide] = useState(false);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>(); const { id, type } = useParams<{ id: string; type: AllClientScopes }>();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
@ -36,7 +45,10 @@ export const ClientScopeForm = () => {
useFetch( useFetch(
async () => { async () => {
if (id) { if (id) {
return await adminClient.clientScopes.findOne({ id }); return {
...(await adminClient.clientScopes.findOne({ id })),
type,
} as ClientScopeDefaultOptionalType;
} }
}, },
(clientScope) => { (clientScope) => {
@ -83,7 +95,7 @@ export const ClientScopeForm = () => {
]; ];
}; };
const save = async (clientScopes: ClientScopeRepresentation) => { const save = async (clientScopes: ClientScopeDefaultOptionalType) => {
try { try {
clientScopes.attributes = convertFormValuesToObject( clientScopes.attributes = convertFormValuesToObject(
clientScopes.attributes! clientScopes.attributes!
@ -91,8 +103,21 @@ export const ClientScopeForm = () => {
if (id) { if (id) {
await adminClient.clientScopes.update({ id }, clientScopes); await adminClient.clientScopes.update({ id }, clientScopes);
changeScope(
adminClient,
{ ...clientScopes, id, type },
clientScopes.type
);
} else { } else {
await adminClient.clientScopes.create(clientScopes); await adminClient.clientScopes.create(clientScopes);
const scope = await adminClient.clientScopes.findOneByName({
name: clientScopes.name!,
});
changeScope(
adminClient,
{ ...clientScopes, id: scope.id },
clientScopes.type
);
} }
addAlert(t((id ? "update" : "create") + "Success"), AlertVariant.success); addAlert(t((id ? "update" : "create") + "Success"), AlertVariant.success);
} catch (error) { } catch (error) {
@ -103,6 +128,24 @@ export const ClientScopeForm = () => {
} }
}; };
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("deleteClientScope", {
count: 1,
name: clientScope?.name,
}),
messageKey: "client-scopes:deleteConfirm",
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.clientScopes.del({ id });
addAlert(t("deletedSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("deleteError", { error }), AlertVariant.danger);
}
},
});
const assignRoles = async (rows: Row[]) => { const assignRoles = async (rows: Row[]) => {
try { try {
const realmRoles = rows const realmRoles = rows
@ -149,11 +192,16 @@ export const ClientScopeForm = () => {
return ( return (
<> <>
<DeleteConfirm />
<ViewHeader <ViewHeader
titleKey={ titleKey={
clientScope ? clientScope.name! : "client-scopes:createClientScope" clientScope ? clientScope.name! : "client-scopes:createClientScope"
} }
subKey="client-scopes:clientScopeExplain" dropdownItems={[
<DropdownItem key="delete" onClick={() => toggleDeleteDialog()}>
{t("common:delete")}
</DropdownItem>,
]}
badge={clientScope ? clientScope.protocol : undefined} badge={clientScope ? clientScope.protocol : undefined}
divider={!id} divider={!id}
/> />

View file

@ -3,6 +3,7 @@
"name": "Name of the client scope. Must be unique in the realm. Name should not contain space characters as it is used as value of scope parameter", "name": "Name of the client scope. Must be unique in the realm. Name should not contain space characters as it is used as value of scope parameter",
"description": "Description of the client scope", "description": "Description of the client scope",
"protocol": "Which SSO protocol configuration is being supplied by this client scope", "protocol": "Which SSO protocol configuration is being supplied by this client scope",
"type": "Client scopes, which will be added as default scopes to each created client",
"displayOnConsentScreen": "If on, and this client scope is added to some client with consent required, the text specified by 'Consent Screen Text' will be displayed on consent screen. If off, this client scope will not be displayed on the consent screen", "displayOnConsentScreen": "If on, and this client scope is added to some client with consent required, the text specified by 'Consent Screen Text' will be displayed on consent screen. If off, this client scope will not be displayed on the consent screen",
"consentScreenText": "Text that will be shown on the consent screen when this client scope is added to some client with consent required. Defaults to name of client scope if it is not filled", "consentScreenText": "Text that will be shown on the consent screen when this client scope is added to some client with consent required. Defaults to name of client scope if it is not filled",
"includeInTokenScope": "If on, the name of this client scope will be added to the access token property 'scope' as well as to the Token Introspection Endpoint response. If off, this client scope will be omitted from the token and from the Token Introspection Endpoint response.", "includeInTokenScope": "If on, the name of this client scope will be added to the access token property 'scope' as well as to the Token Introspection Endpoint response. If off, this client scope will be omitted from the token and from the Token Introspection Endpoint response.",

View file

@ -8,6 +8,7 @@
"searchFor": "Search for client scope", "searchFor": "Search for client scope",
"protocol": "Protocol", "protocol": "Protocol",
"displayOrder": "Display order", "displayOrder": "Display order",
"type": "Type",
"deleteClientScope": "Delete client scope {{name}}", "deleteClientScope": "Delete client scope {{name}}",
"deleteClientScope_plural": "Delete {{count}} client scopes", "deleteClientScope_plural": "Delete {{count}} client scopes",
"deleteConfirm": "Are you sure you want to delete this client scope", "deleteConfirm": "Are you sure you want to delete this client scope",

View file

@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
import { DropdownItem, Select, SelectOption } from "@patternfly/react-core"; import { DropdownItem, Select, SelectOption } from "@patternfly/react-core";
import type ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import type ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import type KeycloakAdminClient from "keycloak-admin";
export enum ClientScope { export enum ClientScope {
default = "default", default = "default",
@ -81,3 +82,48 @@ export const CellDropdown = ({
</Select> </Select>
); );
}; };
export type ClientScopeDefaultOptionalType = ClientScopeRepresentation & {
type: AllClientScopeType;
};
export const changeScope = async (
adminClient: KeycloakAdminClient,
clientScope: ClientScopeDefaultOptionalType,
changeTo: AllClientScopeType
) => {
await removeScope(adminClient, clientScope);
await addScope(adminClient, clientScope, changeTo);
};
const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clientScopes as unknown) as {
[index: string]: Function;
};
export 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!,
});
};

View file

@ -406,7 +406,12 @@ export function KeycloakDataTable<T>({
{loading && <Loading />} {loading && <Loading />}
</PaginatingTableToolbar> </PaginatingTableToolbar>
)} )}
<>{!loading && rows?.length === 0 && search === "" && emptyState}</> <>
{!loading &&
(filteredData || rows)?.length === 0 &&
search === "" &&
emptyState}
</>
</> </>
); );
} }

View file

@ -109,7 +109,7 @@ export const routes: RoutesFn = (t: TFunction) => [
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/:realm/client-scopes/:id/:tab", path: "/:realm/client-scopes/:id/:type/:tab",
component: ClientScopeForm, component: ClientScopeForm,
breadcrumb: t("client-scopes:clientScopeDetails"), breadcrumb: t("client-scopes:clientScopeDetails"),
access: "view-clients", access: "view-clients",