multiple ux issues around realms (#330)

* multiple ux issues around realms

fixes: #324

* changed to same size as scroll spy form width

* fixed test

* fixed realm info link

* fixes console errors

* fixed key

* fixed type

* fixed test introduced cleanup function
This commit is contained in:
Erik Jan de Wit 2021-02-09 13:32:41 +01:00 committed by GitHub
parent 4edcf233f3
commit 3430642003
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 171 additions and 79 deletions

View file

@ -4,3 +4,7 @@
.keycloak__pageheader_brand {
height: 35px;
}
.keycloak__form {
max-width: 1024px;
}

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useHistory } from "react-router-dom";
import React from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useTranslation } from "react-i18next";
import {
Nav,
@ -26,10 +26,6 @@ export const PageNav: React.FunctionComponent = () => {
const history = useHistory();
const initialItem = history.location.pathname;
const [activeItem, setActiveItem] = useState(initialItem);
type SelectedItem = {
groupId: number | string;
itemId: number | string;
@ -38,24 +34,22 @@ export const PageNav: React.FunctionComponent = () => {
};
const onSelect = (item: SelectedItem) => {
setActiveItem(item.to);
history.push(item.to);
item.event.preventDefault();
};
type LeftNavProps = { title: string; path: string };
const LeftNav = ({ title, path }: LeftNavProps) => {
const { realm } = useRealm();
const route = routes(() => {}).find(
(route) => route.path.substr("/:realm".length) === path
);
if (!route || !hasAccess(route.access)) return <></>;
const activeItem = history.location.pathname;
return (
<NavItem
id={"nav-item" + path.replace("/", "-")}
to={`/${realm}${path}`}
isActive={activeItem.substr(realm.length + 1) === path}
isActive={activeItem.substr(activeItem.indexOf("/", 1)) === path}
>
{t(title)}
</NavItem>
@ -76,6 +70,9 @@ export const PageNav: React.FunctionComponent = () => {
"view-identity-providers"
);
const { pathname } = useLocation();
const isOnAddRealm = () => pathname.indexOf("add-realm") === -1;
return (
<PageSidebar
nav={
@ -89,10 +86,12 @@ export const PageNav: React.FunctionComponent = () => {
)}
</DataLoader>
</NavList>
<NavGroup title="">
<LeftNav title="home" path="/" />
</NavGroup>
{showManage && (
{isOnAddRealm() && (
<NavGroup title="">
<LeftNav title="home" path="/" />
</NavGroup>
)}
{showManage && isOnAddRealm() && (
<NavGroup title={t("manage")}>
<LeftNav title="clients" path="/clients" />
<LeftNav title="clientScopes" path="/client-scopes" />
@ -104,7 +103,7 @@ export const PageNav: React.FunctionComponent = () => {
</NavGroup>
)}
{showConfigure && (
{showConfigure && isOnAddRealm() && (
<NavGroup title={t("configure")}>
<LeftNav title="realmSettings" path="/realm-settings" />
<LeftNav title="authentication" path="/authentication" />

View file

@ -3,7 +3,6 @@ import { useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import {
Form,
FormGroup,
PageSection,
Select,
@ -31,6 +30,7 @@ import {
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { FormAccess } from "../../components/form-access/FormAccess";
export const RoleMappingForm = () => {
const { realm } = useContext(RealmContext);
@ -149,7 +149,11 @@ export const RoleMappingForm = () => {
subKey="client-scopes:addMapperExplain"
/>
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<FormGroup
label={t("protocolMapper")}
labelIcon={
@ -302,7 +306,7 @@ export const RoleMappingForm = () => {
{t("common:cancel")}
</Button>
</ActionGroup>
</Form>
</FormAccess>
</PageSection>
</>
);

View file

@ -10,7 +10,6 @@ import {
DropdownItem,
Flex,
FlexItem,
Form,
FormGroup,
PageSection,
Select,
@ -34,6 +33,7 @@ import { useAlerts } from "../../components/alert/Alerts";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { convertFormValuesToObject, convertToFormValues } from "../../util";
import { FormAccess } from "../../components/form-access/FormAccess";
type Params = {
scopeId: string;
@ -154,37 +154,45 @@ export const MappingDetails = () => {
}
/>
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
{!id.match(isGuid) && (
<FormGroup
label={t("common:name")}
labelIcon={
<HelpItem
helpText="client-scopes-help:mapperName"
forLabel={t("common:name")}
forID="name"
/>
}
fieldId="name"
isRequired
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="name"
name="name"
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<>
{!id.match(isGuid) && (
<FormGroup
label={t("common:name")}
labelIcon={
<HelpItem
helpText="client-scopes-help:mapperName"
forLabel={t("common:name")}
forID="name"
/>
}
fieldId="name"
isRequired
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="name"
name="name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
</>
<FormGroup
label={t("realmRolePrefix")}
labelIcon={
@ -348,7 +356,7 @@ export const MappingDetails = () => {
</Button>
<Button variant="link">{t("common:cancel")}</Button>
</ActionGroup>
</Form>
</FormAccess>
</PageSection>
</>
);

View file

@ -1,7 +1,6 @@
import React, { useState } from "react";
import {
FormGroup,
Form,
Select,
SelectVariant,
SelectOption,
@ -11,6 +10,7 @@ import { Controller, UseFormMethods } from "react-hook-form";
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
import { ClientDescription } from "../ClientDescription";
import { FormAccess } from "../../components/form-access/FormAccess";
type GeneralSettingsProps = {
form: UseFormMethods;
@ -24,7 +24,7 @@ export const GeneralSettings = ({ form }: GeneralSettingsProps) => {
const [open, isOpen] = useState(false);
return (
<Form isHorizontal>
<FormAccess isHorizontal role="manage-clients">
<FormGroup
label="Client Type"
fieldId="kc-type"
@ -64,6 +64,6 @@ export const GeneralSettings = ({ form }: GeneralSettingsProps) => {
/>
</FormGroup>
<ClientDescription form={form} />
</Form>
</FormAccess>
);
};

View file

@ -6,7 +6,6 @@ import {
CardBody,
ClipboardCopy,
Divider,
Form,
FormGroup,
Select,
SelectOption,
@ -20,6 +19,7 @@ import { Controller, UseFormMethods, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAlerts } from "../../components/alert/Alerts";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
import { FormAccess } from "../../components/form-access/FormAccess";
import { HelpItem } from "../../components/help-enabler/HelpItem";
import {
@ -136,7 +136,7 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
});
return (
<Form isHorizontal className="pf-u-mt-md">
<FormAccess isHorizontal className="pf-u-mt-md" role="manage-clients">
<ClientSecretConfirm />
<AccessTokenConfirm />
<Card isFlat>
@ -237,6 +237,6 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
</FormGroup>
</CardBody>
</Card>
</Form>
</FormAccess>
);
};

View file

@ -1,7 +1,6 @@
import React from "react";
import {
PageSection,
Form,
FormGroup,
TextInput,
ActionGroup,
@ -17,6 +16,7 @@ import { useAlerts } from "../../components/alert/Alerts";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
import { FormAccess } from "../../components/form-access/FormAccess";
export const ImportForm = () => {
const { t } = useTranslation("clients");
@ -55,7 +55,11 @@ export const ImportForm = () => {
subKey="clients:clientsExplain"
/>
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<JsonFileUpload id="realm-file" onChange={handleFileChange} />
<ClientDescription form={form} />
<FormGroup label={t("common:type")} fieldId="kc-type">
@ -73,7 +77,7 @@ export const ImportForm = () => {
</Button>
<Button variant="link">{t("common:cancel")}</Button>
</ActionGroup>
</Form>
</FormAccess>
</PageSection>
</>
);

View file

@ -22,7 +22,9 @@
"action": "Action",
"download": "Download",
"resourceFile": "Resource file",
"clear": "Clear",
"clearFile": "Clear this file",
"clearFileExplain": "Are you sure you want to clear this file?",
"on": "On",
"off": "Off",
"enabled": "Enabled",

View file

@ -111,7 +111,7 @@ export const FormAccess = ({
return (
<>
{!unWrap && (
<Form {...rest}>
<Form {...rest} className={"keycloak__form " + (rest.className || "")}>
{recursiveCloneChildren(children, isDisabled ? { isDisabled } : {})}
</Form>
)}

View file

@ -8,9 +8,11 @@ exports[`<FormAccess /> render normal form 1`] = `
<FormAccess
role="manage-clients"
>
<Form>
<Form
className="keycloak__form "
>
<form
className="pf-c-form"
className="pf-c-form keycloak__form "
noValidate={true}
>
<FormGroup

View file

@ -85,7 +85,7 @@ export const JsonFileUpload = ({
</Button>,
]}
>
{t("Are you sure you want to clear this file?")}
{t("clearFileExplain")}
</Modal>
)}
<FormGroup

View file

@ -74,7 +74,7 @@ export const RealmSelector = ({ realmList }: RealmSelectorProps) => {
key={`realm-dropdown-item-${r.realm}`}
onClick={() => {
setRealm(r.realm!);
history.push(`/${r.realm}`);
history.push(`/${r.realm}/`);
setOpen(!open);
}}
>

View file

@ -1,6 +1,7 @@
.kc-form-panel__panel {
padding-top: var(--pf-global--spacer--lg);
padding-bottom: var(--pf-global--spacer--2xl);
max-width: 768px;
}
.kc-form-panel__title {

View file

@ -51,7 +51,7 @@ const EmptyDashboard = () => {
</Title>
<EmptyStateBody>{t("introduction")}</EmptyStateBody>
<Button variant="link" onClick={() => setRealm("master")}>
{t("common:serverInfo")}
{t("common:realmInfo")}
</Button>
</EmptyState>
</PageSection>

View file

@ -74,7 +74,7 @@ const RealmSettingsHeader = ({
await adminClient.realms.del({ realm: realmName });
addAlert(t("deletedSuccess"), AlertVariant.success);
setRealm("master");
history.push("/master");
history.push("/master/");
} catch (error) {
addAlert(t("deleteError", { error }), AlertVariant.danger);
}

View file

@ -19,6 +19,7 @@ import { ViewHeader } from "../../components/view-header/ViewHeader";
import RealmRepresentation from "keycloak-admin/lib/defs/realmRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
import { WhoAmIContext } from "../../context/whoami/WhoAmI";
import { FormAccess } from "../../components/form-access/FormAccess";
export const NewRealmForm = () => {
const { t } = useTranslation("realm");
@ -27,14 +28,26 @@ export const NewRealmForm = () => {
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const { register, handleSubmit, setValue, control } = useForm<
RealmRepresentation
>();
const {
register,
handleSubmit,
setValue,
control,
formState,
errors,
} = useForm<RealmRepresentation>({ mode: "onChange" });
const handleFileChange = (value: string | File) => {
const defaultRealm = { id: "", realm: "", enabled: true };
const obj = value ? JSON.parse(value as string) : defaultRealm;
let obj: { [name: string]: boolean | string } = defaultRealm;
if (value) {
try {
obj = JSON.parse(value as string);
} catch (error) {
console.warn("Invalid json, ignoring value using default");
}
}
Object.keys(obj).forEach((k) => {
setValue(k, obj[k]);
});
@ -43,14 +56,16 @@ export const NewRealmForm = () => {
const save = async (realm: RealmRepresentation) => {
try {
await adminClient.realms.create(realm);
addAlert(t("Realm created"), AlertVariant.success);
addAlert(t("saveRealmSuccess"), AlertVariant.success);
refresh();
//force token update
await adminClient.keycloak?.updateToken(Number.MAX_VALUE);
history.push(`/${realm.realm}`);
history.push(`/${realm.realm}/`);
} catch (error) {
addAlert(
`${t("Could not create realm:")} '${error}'`,
t("saveRealmError", {
error: error.response.data?.errorMessage || error,
}),
AlertVariant.danger
);
}
@ -60,14 +75,25 @@ export const NewRealmForm = () => {
<>
<ViewHeader titleKey="realm:createRealm" subKey="realm:realmExplain" />
<PageSection variant="light">
<Form isHorizontal onSubmit={handleSubmit(save)}>
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-realm"
>
<JsonFileUpload id="kc-realm-filename" onChange={handleFileChange} />
<FormGroup label={t("realmName")} isRequired fieldId="kc-realm-name">
<FormGroup
label={t("realmName")}
isRequired
fieldId="kc-realm-name"
validated={errors.realm ? "error" : "default"}
helperTextInvalid={t("common:required")}
>
<TextInput
isRequired
type="text"
id="kc-realm-name"
name="realm"
validated={errors.realm ? "error" : "default"}
ref={register({ required: true })}
/>
</FormGroup>
@ -89,12 +115,18 @@ export const NewRealmForm = () => {
/>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
<Button
variant="primary"
type="submit"
isDisabled={!formState.isValid}
>
{t("common:create")}
</Button>
<Button variant="link">{t("common:cancel")}</Button>
<Button variant="link" onClick={() => history.goBack()}>
{t("common:cancel")}
</Button>
</ActionGroup>
</Form>
</FormAccess>
</PageSection>
</>
);

View file

@ -6,6 +6,8 @@
"createRealm": "Create realm",
"realmExplain": "A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs into a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.",
"noRealmRoles": "No realm roles",
"emptyStateText": "There aren't any realm roles in this realm. Create a realm role to get started."
"emptyStateText": "There aren't any realm roles in this realm. Create a realm role to get started.",
"saveRealmSuccess": "Realm created successfully",
"saveRealmError": "Could not create realm {{error}}"
}
}

View file

@ -2,6 +2,7 @@ import LoginPage from "../support/pages/LoginPage";
import SidebarPage from "../support/pages/admin_console/SidebarPage";
import CreateRealmPage from "../support/pages/admin_console/CreateRealmPage";
import Masthead from "../support/pages/admin_console/Masthead";
import AdminClient from "../support/util/AdminClient";
const masthead = new Masthead();
const loginPage = new LoginPage();
@ -9,25 +10,30 @@ const sidebarPage = new SidebarPage();
const createRealmPage = new CreateRealmPage();
describe("Realms test", function () {
const testRealmName = "Test realm";
describe("Realm creation", function () {
beforeEach(function () {
cy.visit("");
loginPage.logIn();
});
after(async () => {
const client = new AdminClient();
await client.deleteRealm(testRealmName);
});
it("should fail creating Master realm", function () {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName("master").createRealm();
masthead.checkNotificationMessage(
"Error: Request failed with status code 409"
"Could not create realm Conflict detected. See logs for details"
);
});
it("should create Test realm", function () {
sidebarPage.goToCreateRealm();
createRealmPage.fillRealmName("Test realm").createRealm();
createRealmPage.fillRealmName(testRealmName).createRealm();
masthead.checkNotificationMessage("Realm created");
});
@ -35,7 +41,10 @@ describe("Realms test", function () {
it("should change to Test realm", function () {
sidebarPage.getCurrentRealm().should("eq", "Master");
sidebarPage.goToRealm("Test realm").getCurrentRealm().should("eq", "Test realm");
sidebarPage
.goToRealm(testRealmName)
.getCurrentRealm()
.should("eq", testRealmName);
});
});
});

View file

@ -0,0 +1,25 @@
import KeycloakAdminClient from "keycloak-admin";
export default class AdminClient {
private client: KeycloakAdminClient;
constructor() {
this.client = new KeycloakAdminClient({
baseUrl: "http://localhost:8180/auth",
realmName: "master",
});
}
private async login() {
await this.client.auth({
username: "admin",
password: "admin",
grantType: "password",
clientId: "admin-cli",
});
}
async deleteRealm(realm: string) {
await this.login();
await this.client.realms.del({ realm });
}
}