Changes from Realm Roles UX Review [List] (#433)
* realm roles UX review progress wip * filter realm roles on Enter key press, add filter functionality * remove chip group filters * clean up * format * filterChips logic now in table toolbar * fix lint and format * save with erik * remove filter chips functionality * fix check-types * fix realm roles cypress test * format * revert changes to group attributes * cypress test * use filter * remove log * remove unused prop
This commit is contained in:
parent
6c399c1484
commit
bf4cae6735
16 changed files with 294 additions and 70 deletions
|
@ -73,8 +73,6 @@ describe("Realm roles test", function () {
|
||||||
|
|
||||||
masthead.checkNotificationMessage("Role created");
|
masthead.checkNotificationMessage("Role created");
|
||||||
|
|
||||||
cy.wait(100);
|
|
||||||
|
|
||||||
// Add associated realm role
|
// Add associated realm role
|
||||||
|
|
||||||
associatedRolesPage.addAssociatedRealmRole();
|
associatedRolesPage.addAssociatedRealmRole();
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default class AssociatedRolesPage {
|
||||||
|
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
|
|
||||||
cy.get(this.checkbox).eq(1).check();
|
cy.get(this.checkbox).eq(2).check();
|
||||||
|
|
||||||
cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
|
cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ export default class AssociatedRolesPage {
|
||||||
|
|
||||||
cy.wait(2500);
|
cy.wait(2500);
|
||||||
|
|
||||||
cy.get(this.checkbox).eq(40).check({ force: true });
|
cy.get(this.checkbox).eq(12).check({ force: true });
|
||||||
|
|
||||||
cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
|
cy.get(this.addAssociatedRolesModalButton).contains("Add").click();
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default class CreateRealmRolePage {
|
||||||
this.realmRoleNameError = "#kc-name-helper";
|
this.realmRoleNameError = "#kc-name-helper";
|
||||||
this.realmRoleDescriptionInput = "#kc-role-description";
|
this.realmRoleDescriptionInput = "#kc-role-description";
|
||||||
|
|
||||||
this.saveBtn = '[type="submit"]';
|
this.saveBtn = 'realm-roles-save-button';
|
||||||
this.cancelBtn = '[type="button"]';
|
this.cancelBtn = '[type="button"]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export default class CreateRealmRolePage {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
cy.get(this.saveBtn).click();
|
cy.getId(this.saveBtn).click();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,9 +58,9 @@ export const attributesToArray = (attributes?: {
|
||||||
|
|
||||||
export const AttributesForm = ({
|
export const AttributesForm = ({
|
||||||
form: { handleSubmit, register, formState, errors, watch },
|
form: { handleSubmit, register, formState, errors, watch },
|
||||||
save,
|
|
||||||
array: { fields, append, remove },
|
array: { fields, append, remove },
|
||||||
reset,
|
reset,
|
||||||
|
save,
|
||||||
}: AttributesFormProps) => {
|
}: AttributesFormProps) => {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ export const AttributesForm = ({
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
name={`attributes[${rowIndex}].key`}
|
name={`attributes[${rowIndex}].key`}
|
||||||
ref={register({ required: true })}
|
ref={register()}
|
||||||
aria-label="key-input"
|
aria-label="key-input"
|
||||||
defaultValue={attribute.key}
|
defaultValue={attribute.key}
|
||||||
validated={
|
validated={
|
||||||
|
|
|
@ -139,7 +139,7 @@ export function KeycloakDataTable<T>({
|
||||||
|
|
||||||
const [max, setMax] = useState(10);
|
const [max, setMax] = useState(10);
|
||||||
const [first, setFirst] = useState(0);
|
const [first, setFirst] = useState(0);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState<string>("");
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(new Date().getTime());
|
const refresh = () => setKey(new Date().getTime());
|
||||||
|
@ -172,7 +172,7 @@ export function KeycloakDataTable<T>({
|
||||||
},
|
},
|
||||||
handleError
|
handleError
|
||||||
);
|
);
|
||||||
}, [key, first, max]);
|
}, [key, first, max, search]);
|
||||||
|
|
||||||
const getNodeText = (node: keyof T | JSX.Element): string => {
|
const getNodeText = (node: keyof T | JSX.Element): string => {
|
||||||
if (["string", "number"].includes(typeof node)) {
|
if (["string", "number"].includes(typeof node)) {
|
||||||
|
@ -197,6 +197,7 @@ export function KeycloakDataTable<T>({
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
setSearch;
|
||||||
};
|
};
|
||||||
|
|
||||||
const convertAction = () =>
|
const convertAction = () =>
|
||||||
|
@ -214,13 +215,6 @@ export function KeycloakDataTable<T>({
|
||||||
return action;
|
return action;
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchOnChange = (value: string) => {
|
|
||||||
if (value === "") {
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
setSearch(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Loading = () => (
|
const Loading = () => (
|
||||||
<div className="pf-u-text-align-center">
|
<div className="pf-u-text-align-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -258,8 +252,7 @@ export function KeycloakDataTable<T>({
|
||||||
inputGroupName={
|
inputGroupName={
|
||||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||||
}
|
}
|
||||||
inputGroupOnChange={searchOnChange}
|
inputGroupOnEnter={setSearch}
|
||||||
inputGroupOnClick={refresh}
|
|
||||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||||
searchTypeComponent={searchTypeComponent}
|
searchTypeComponent={searchTypeComponent}
|
||||||
toolbarItem={toolbarItem}
|
toolbarItem={toolbarItem}
|
||||||
|
@ -291,8 +284,7 @@ export function KeycloakDataTable<T>({
|
||||||
inputGroupName={
|
inputGroupName={
|
||||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||||
}
|
}
|
||||||
inputGroupOnChange={searchOnChange}
|
inputGroupOnEnter={(search) => filter(search)}
|
||||||
inputGroupOnClick={() => filter(search)}
|
|
||||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||||
toolbarItem={toolbarItem}
|
toolbarItem={toolbarItem}
|
||||||
searchTypeComponent={searchTypeComponent}
|
searchTypeComponent={searchTypeComponent}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { MouseEventHandler } from "react";
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
ToggleTemplateProps,
|
ToggleTemplateProps,
|
||||||
|
@ -22,7 +22,7 @@ type TableToolbarProps = {
|
||||||
newInput: string,
|
newInput: string,
|
||||||
event: React.FormEvent<HTMLInputElement>
|
event: React.FormEvent<HTMLInputElement>
|
||||||
) => void;
|
) => void;
|
||||||
inputGroupOnClick?: MouseEventHandler;
|
inputGroupOnEnter?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PaginatingTableToolbar = ({
|
export const PaginatingTableToolbar = ({
|
||||||
|
@ -38,7 +38,7 @@ export const PaginatingTableToolbar = ({
|
||||||
inputGroupName,
|
inputGroupName,
|
||||||
inputGroupPlaceholder,
|
inputGroupPlaceholder,
|
||||||
inputGroupOnChange,
|
inputGroupOnChange,
|
||||||
inputGroupOnClick,
|
inputGroupOnEnter,
|
||||||
}: TableToolbarProps) => {
|
}: TableToolbarProps) => {
|
||||||
const page = Math.round(first / max);
|
const page = Math.round(first / max);
|
||||||
const pagination = (variant: "top" | "bottom" = "top") => (
|
const pagination = (variant: "top" | "bottom" = "top") => (
|
||||||
|
@ -59,24 +59,23 @@ export const PaginatingTableToolbar = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
<>{children}</>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<TableToolbar
|
<TableToolbar
|
||||||
searchTypeComponent={searchTypeComponent}
|
searchTypeComponent={searchTypeComponent}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
{toolbarItem}
|
{toolbarItem}
|
||||||
{count !== 0 && (
|
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
|
||||||
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
toolbarItemFooter={
|
toolbarItemFooter={<ToolbarItem>{pagination("bottom")}</ToolbarItem>}
|
||||||
count !== 0 && <ToolbarItem>{pagination("bottom")}</ToolbarItem>
|
|
||||||
}
|
|
||||||
inputGroupName={inputGroupName}
|
inputGroupName={inputGroupName}
|
||||||
inputGroupPlaceholder={inputGroupPlaceholder}
|
inputGroupPlaceholder={inputGroupPlaceholder}
|
||||||
inputGroupOnChange={inputGroupOnChange}
|
inputGroupOnChange={inputGroupOnChange}
|
||||||
inputGroupOnClick={inputGroupOnClick}
|
inputGroupOnEnter={inputGroupOnEnter}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</TableToolbar>
|
</TableToolbar>
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
import React, {
|
import React, { FormEvent, Fragment, ReactNode } from "react";
|
||||||
FormEvent,
|
|
||||||
Fragment,
|
|
||||||
MouseEventHandler,
|
|
||||||
ReactNode,
|
|
||||||
} from "react";
|
|
||||||
import {
|
import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarContent,
|
ToolbarContent,
|
||||||
|
@ -28,7 +23,7 @@ type TableToolbarProps = {
|
||||||
newInput: string,
|
newInput: string,
|
||||||
event: FormEvent<HTMLInputElement>
|
event: FormEvent<HTMLInputElement>
|
||||||
) => void;
|
) => void;
|
||||||
inputGroupOnClick?: MouseEventHandler;
|
inputGroupOnEnter?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TableToolbar = ({
|
export const TableToolbar = ({
|
||||||
|
@ -39,9 +34,35 @@ export const TableToolbar = ({
|
||||||
inputGroupName,
|
inputGroupName,
|
||||||
inputGroupPlaceholder,
|
inputGroupPlaceholder,
|
||||||
inputGroupOnChange,
|
inputGroupOnChange,
|
||||||
inputGroupOnClick,
|
inputGroupOnEnter,
|
||||||
}: TableToolbarProps) => {
|
}: TableToolbarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [searchValue, setSearchValue] = React.useState<string>("");
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
if (searchValue !== "") {
|
||||||
|
setSearchValue(searchValue);
|
||||||
|
inputGroupOnEnter && inputGroupOnEnter(searchValue);
|
||||||
|
} else {
|
||||||
|
setSearchValue("");
|
||||||
|
inputGroupOnEnter && inputGroupOnEnter("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: any) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
onSearch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
value: string,
|
||||||
|
event: FormEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
inputGroupOnChange && inputGroupOnChange(value, event);
|
||||||
|
setSearchValue(value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
@ -59,12 +80,13 @@ export const TableToolbar = ({
|
||||||
type="search"
|
type="search"
|
||||||
aria-label={t("search")}
|
aria-label={t("search")}
|
||||||
placeholder={inputGroupPlaceholder}
|
placeholder={inputGroupPlaceholder}
|
||||||
onChange={inputGroupOnChange}
|
onChange={handleInputChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant={ButtonVariant.control}
|
variant={ButtonVariant.control}
|
||||||
aria-label={t("search")}
|
aria-label={t("search")}
|
||||||
onClick={inputGroupOnClick}
|
onClick={onSearch}
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -123,8 +123,6 @@ export const AssociatedRolesTab = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(inheritanceMap);
|
|
||||||
|
|
||||||
const toggleModal = () => setOpen(!open);
|
const toggleModal = () => setOpen(!open);
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
@ -173,7 +171,7 @@ export const AssociatedRolesTab = ({
|
||||||
const goToCreate = () => history.push(`${url}/add-role`);
|
const goToCreate = () => history.push(`${url}/add-role`);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSection variant="light">
|
<PageSection variant="light" padding={{ default: "noPadding" }}>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
<DeleteAssociatedRolesConfirm />
|
<DeleteAssociatedRolesConfirm />
|
||||||
<AssociatedRolesModal
|
<AssociatedRolesModal
|
||||||
|
|
|
@ -9,28 +9,28 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UseFormMethods } from "react-hook-form";
|
import { UseFormMethods } from "react-hook-form";
|
||||||
|
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
|
||||||
import { RoleFormType } from "./RealmRoleTabs";
|
import { RoleFormType } from "./RealmRoleTabs";
|
||||||
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
|
||||||
export type RealmRoleFormProps = {
|
export type RealmRoleFormProps = {
|
||||||
form: UseFormMethods<RoleFormType>;
|
form: UseFormMethods<RoleFormType>;
|
||||||
save: (role: RoleFormType) => void;
|
save: () => void;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RealmRoleForm = ({
|
export const RealmRoleForm = ({
|
||||||
form,
|
form: { handleSubmit, errors, register },
|
||||||
save,
|
save,
|
||||||
editMode,
|
editMode,
|
||||||
reset,
|
reset,
|
||||||
}: RealmRoleFormProps) => {
|
}: RealmRoleFormProps) => {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAccess
|
<FormAccess
|
||||||
isHorizontal
|
isHorizontal
|
||||||
onSubmit={form.handleSubmit(save)}
|
onSubmit={handleSubmit(save)}
|
||||||
role="manage-realm"
|
role="manage-realm"
|
||||||
className="pf-u-mt-lg"
|
className="pf-u-mt-lg"
|
||||||
>
|
>
|
||||||
|
@ -38,11 +38,11 @@ export const RealmRoleForm = ({
|
||||||
label={t("roleName")}
|
label={t("roleName")}
|
||||||
fieldId="kc-name"
|
fieldId="kc-name"
|
||||||
isRequired
|
isRequired
|
||||||
validated={form.errors.name ? "error" : "default"}
|
validated={errors.name ? "error" : "default"}
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={form.register({ required: !editMode })}
|
ref={register({ required: !editMode })}
|
||||||
type="text"
|
type="text"
|
||||||
id="kc-name"
|
id="kc-name"
|
||||||
name="name"
|
name="name"
|
||||||
|
@ -53,15 +53,13 @@ export const RealmRoleForm = ({
|
||||||
label={t("common:description")}
|
label={t("common:description")}
|
||||||
fieldId="kc-description"
|
fieldId="kc-description"
|
||||||
validated={
|
validated={
|
||||||
form.errors.description
|
errors.description ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
}
|
||||||
helperTextInvalid={form.errors.description?.message}
|
helperTextInvalid={errors.description?.message}
|
||||||
>
|
>
|
||||||
<TextArea
|
<TextArea
|
||||||
name="description"
|
name="description"
|
||||||
ref={form.register({
|
ref={register({
|
||||||
maxLength: {
|
maxLength: {
|
||||||
value: 255,
|
value: 255,
|
||||||
message: t("common:maxLength", { length: 255 }),
|
message: t("common:maxLength", { length: 255 }),
|
||||||
|
@ -69,7 +67,7 @@ export const RealmRoleForm = ({
|
||||||
})}
|
})}
|
||||||
type="text"
|
type="text"
|
||||||
validated={
|
validated={
|
||||||
form.errors.description
|
errors.description
|
||||||
? ValidatedOptions.error
|
? ValidatedOptions.error
|
||||||
: ValidatedOptions.default
|
: ValidatedOptions.default
|
||||||
}
|
}
|
||||||
|
@ -77,7 +75,11 @@ export const RealmRoleForm = ({
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<Button variant="primary" type="submit">
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={save}
|
||||||
|
data-testid="realm-roles-save-button"
|
||||||
|
>
|
||||||
{t("common:save")}
|
{t("common:save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => reset()} variant="link">
|
<Button onClick={() => reset()} variant="link">
|
||||||
|
|
|
@ -93,8 +93,21 @@ export const RealmRoleTabs = () => {
|
||||||
|
|
||||||
useEffect(() => append({ key: "", value: "" }), [append, role]);
|
useEffect(() => append({ key: "", value: "" }), [append, role]);
|
||||||
|
|
||||||
const save = async (role: RoleFormType) => {
|
const save = async () => {
|
||||||
try {
|
try {
|
||||||
|
const role = form.getValues();
|
||||||
|
if (
|
||||||
|
role.attributes &&
|
||||||
|
role.attributes[role.attributes.length - 1].key === ""
|
||||||
|
) {
|
||||||
|
form.setValue(
|
||||||
|
"attributes",
|
||||||
|
role.attributes.slice(0, role.attributes.length - 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!(await form.trigger())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { attributes, ...rest } = role;
|
const { attributes, ...rest } = role;
|
||||||
const roleRepresentation: RoleRepresentation = rest;
|
const roleRepresentation: RoleRepresentation = rest;
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|
|
@ -28,3 +28,8 @@
|
||||||
padding-right: var(--pf-global--spacer--xs);
|
padding-right: var(--pf-global--spacer--xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pf-c-chip-group.kc-filter-chip-group__table {
|
||||||
|
margin-left: var(--pf-global--spacer--md);
|
||||||
|
margin-bottom: var(--pf-global--spacer--md);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,51 @@
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { PageSection } from "@patternfly/react-core";
|
import { PageSection } from "@patternfly/react-core";
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
|
||||||
import { RolesList } from "./RolesList";
|
import { RolesList } from "./RolesList";
|
||||||
|
import { useErrorHandler } from "react-error-boundary";
|
||||||
|
|
||||||
export const RealmRolesSection = () => {
|
export const RealmRolesSection = () => {
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
|
const [listRoles, setListRoles] = useState(false);
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return asyncStateFetch(
|
||||||
|
() => {
|
||||||
|
return Promise.all([adminClient.roles.find()]);
|
||||||
|
},
|
||||||
|
|
||||||
|
(response) => {
|
||||||
|
setListRoles(!(response[0] && response[0].length > 0));
|
||||||
|
},
|
||||||
|
handleError
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const loader = async (first?: number, max?: number, search?: string) => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const params: { [name: string]: string | number } = {
|
const params: { [name: string]: string | number } = {
|
||||||
first: first!,
|
first: first!,
|
||||||
max: max!,
|
max: max!,
|
||||||
search: search!,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const searchParam = search || "";
|
||||||
|
|
||||||
|
if (searchParam) {
|
||||||
|
params.search = searchParam;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listRoles) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return await adminClient.roles.find(params);
|
return await adminClient.roles.find(params);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" />
|
<ViewHeader titleKey="roles:title" subKey="roles:roleExplain" />
|
||||||
<PageSection variant="light">
|
<PageSection variant="light" padding={{ default: "noPadding" }}>
|
||||||
<RolesList loader={loader} />
|
<RolesList loader={loader} />
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
|
|
160
src/realm-roles/RoleAttributes.tsx
Normal file
160
src/realm-roles/RoleAttributes.tsx
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ArrayField, UseFormMethods } from "react-hook-form";
|
||||||
|
import { ActionGroup, Button, TextInput } from "@patternfly/react-core";
|
||||||
|
import {
|
||||||
|
TableComposable,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@patternfly/react-table";
|
||||||
|
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
import { RoleFormType } from "./RealmRoleTabs";
|
||||||
|
|
||||||
|
import "./RealmRolesSection.css";
|
||||||
|
|
||||||
|
export type KeyValueType = { key: string; value: string };
|
||||||
|
|
||||||
|
type RoleAttributesProps = {
|
||||||
|
form: UseFormMethods<RoleFormType>;
|
||||||
|
save: () => void;
|
||||||
|
reset: () => void;
|
||||||
|
array: {
|
||||||
|
fields: Partial<ArrayField<Record<string, any>, "id">>[];
|
||||||
|
append: (
|
||||||
|
value: Partial<Record<string, any>> | Partial<Record<string, any>>[],
|
||||||
|
shouldFocus?: boolean | undefined
|
||||||
|
) => void;
|
||||||
|
remove: (index?: number | number[] | undefined) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RoleAttributes = ({
|
||||||
|
form: { register, formState, errors, watch },
|
||||||
|
save,
|
||||||
|
array: { fields, append, remove },
|
||||||
|
reset,
|
||||||
|
}: RoleAttributesProps) => {
|
||||||
|
const { t } = useTranslation("roles");
|
||||||
|
|
||||||
|
const columns = ["Key", "Value"];
|
||||||
|
const watchFirstKey = watch("attributes[0].key", "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormAccess role="manage-realm">
|
||||||
|
<TableComposable
|
||||||
|
className="kc-role-attributes__table"
|
||||||
|
aria-label="Role attribute keys and values"
|
||||||
|
variant="compact"
|
||||||
|
borders={false}
|
||||||
|
>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th id="key" width={40}>
|
||||||
|
{columns[0]}
|
||||||
|
</Th>
|
||||||
|
<Th id="value" width={40}>
|
||||||
|
{columns[1]}
|
||||||
|
</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{fields.map((attribute, rowIndex) => (
|
||||||
|
<Tr key={attribute.id}>
|
||||||
|
<Td
|
||||||
|
key={`${attribute.id}-key`}
|
||||||
|
id={`text-input-${rowIndex}-key`}
|
||||||
|
dataLabel={columns[0]}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
name={`attributes[${rowIndex}].key`}
|
||||||
|
ref={register()}
|
||||||
|
aria-label="key-input"
|
||||||
|
defaultValue={attribute.key}
|
||||||
|
validated={
|
||||||
|
errors.attributes && errors.attributes[rowIndex]
|
||||||
|
? "error"
|
||||||
|
: "default"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<Td
|
||||||
|
key={`${attribute}-value`}
|
||||||
|
id={`text-input-${rowIndex}-value`}
|
||||||
|
dataLabel={columns[1]}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
name={`attributes[${rowIndex}].value`}
|
||||||
|
ref={register()}
|
||||||
|
aria-label="value-input"
|
||||||
|
defaultValue={attribute.value}
|
||||||
|
validated={errors.description ? "error" : "default"}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
{rowIndex !== fields.length - 1 && fields.length - 1 !== 0 && (
|
||||||
|
<Td
|
||||||
|
key="minus-button"
|
||||||
|
id={`kc-minus-button-${rowIndex}`}
|
||||||
|
dataLabel={columns[2]}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id={`minus-button-${rowIndex}`}
|
||||||
|
aria-label={`remove ${attribute.key} with value ${attribute.value} `}
|
||||||
|
variant="link"
|
||||||
|
className="kc-role-attributes__minus-icon"
|
||||||
|
onClick={() => remove(rowIndex)}
|
||||||
|
>
|
||||||
|
<MinusCircleIcon />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
{rowIndex === fields.length - 1 && (
|
||||||
|
<Td key="add-button" id="add-button" dataLabel={columns[2]}>
|
||||||
|
{fields[rowIndex].key === "" && (
|
||||||
|
<Button
|
||||||
|
id={`minus-button-${rowIndex}`}
|
||||||
|
aria-label={`remove ${attribute.key} with value ${attribute.value} `}
|
||||||
|
variant="link"
|
||||||
|
className="kc-role-attributes__minus-icon"
|
||||||
|
onClick={() => remove(rowIndex)}
|
||||||
|
>
|
||||||
|
<MinusCircleIcon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
aria-label={t("roles:addAttributeText")}
|
||||||
|
id="plus-icon"
|
||||||
|
variant="link"
|
||||||
|
className="kc-role-attributes__plus-icon"
|
||||||
|
onClick={() => append({ key: "", value: "" })}
|
||||||
|
icon={<PlusCircleIcon />}
|
||||||
|
isDisabled={!formState.isValid}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
<ActionGroup className="kc-role-attributes__action-group">
|
||||||
|
<Button
|
||||||
|
data-testid="realm-roles-save-button"
|
||||||
|
variant="primary"
|
||||||
|
isDisabled={!watchFirstKey}
|
||||||
|
onClick={save}
|
||||||
|
>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={reset} variant="link">
|
||||||
|
{t("common:reload")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormAccess>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -79,6 +79,7 @@ export const RolesList = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const goToCreate = () => history.push(`${url}/add-role`);
|
const goToCreate = () => history.push(`${url}/add-role`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
|
|
|
@ -66,6 +66,7 @@
|
||||||
"userName": "Username",
|
"userName": "Username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"lastName": "Last name",
|
"lastName": "Last name",
|
||||||
"firstName": "First name"
|
"firstName": "First name",
|
||||||
|
"clearAllFilters": "Clear all filters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ export const UsersSection = () => {
|
||||||
const [listUsers, setListUsers] = useState(false);
|
const [listUsers, setListUsers] = useState(false);
|
||||||
const [initialSearch, setInitialSearch] = useState("");
|
const [initialSearch, setInitialSearch] = useState("");
|
||||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const [key, setKey] = useState("");
|
const [key, setKey] = useState("");
|
||||||
const refresh = () => setKey(`${new Date().getTime()}`);
|
const refresh = () => setKey(`${new Date().getTime()}`);
|
||||||
|
@ -87,6 +88,7 @@ export const UsersSection = () => {
|
||||||
const searchParam = search || initialSearch || "";
|
const searchParam = search || initialSearch || "";
|
||||||
if (searchParam) {
|
if (searchParam) {
|
||||||
params.search = searchParam;
|
params.search = searchParam;
|
||||||
|
setSearch(searchParam);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listUsers && !searchParam) {
|
if (!listUsers && !searchParam) {
|
||||||
|
@ -192,12 +194,16 @@ export const UsersSection = () => {
|
||||||
canSelectAll
|
canSelectAll
|
||||||
onSelect={(rows) => setSelectedRows([...rows])}
|
onSelect={(rows) => setSelectedRows([...rows])}
|
||||||
emptyState={
|
emptyState={
|
||||||
<ListEmptyState
|
!search ? (
|
||||||
message={t("noUsersFound")}
|
<ListEmptyState
|
||||||
instructions={t("emptyInstructions")}
|
message={t("noUsersFound")}
|
||||||
primaryActionText={t("createNewUser")}
|
instructions={t("emptyInstructions")}
|
||||||
onPrimaryAction={goToCreate}
|
primaryActionText={t("createNewUser")}
|
||||||
/>
|
onPrimaryAction={goToCreate}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in a new issue