Grouped multiple credentials (#1700)

* expandable rows - wip

* expandable rows - wip

* expandable rows - wip

* expandable rows - wip

* expandable rows - wip

* expandable rows - wip

* expandable rows - wip

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* expandable rows - css improvements

* removed unnecessary css

* css cleanup

* feedback fixes

* table refactor

* table refactor

* table refactor

* table refactor

* table refactor

* small css fix

Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com>
This commit is contained in:
agagancarczyk 2022-01-10 10:31:50 +00:00 committed by GitHub
parent 6c32d69e46
commit 1f45fb89aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 406 additions and 198 deletions

View file

@ -1,4 +1,4 @@
import React, { FunctionComponent, useMemo, useState } from "react"; import React, { Fragment, FunctionComponent, useMemo, useState } from "react";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -27,7 +27,6 @@ import {
TableComposable, TableComposable,
TableHeader, TableHeader,
TableVariant, TableVariant,
Tbody,
Td, Td,
Th, Th,
Thead, Thead,
@ -44,7 +43,7 @@ import { useWhoAmI } from "../context/whoami/WhoAmI";
import { Controller, useForm, UseFormMethods, useWatch } from "react-hook-form"; import { Controller, useForm, UseFormMethods, useWatch } from "react-hook-form";
import { PasswordInput } from "../components/password-input/PasswordInput"; import { PasswordInput } from "../components/password-input/PasswordInput";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import "./user-section.css"; import "./user-credentials.css";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation"; import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
@ -90,6 +89,12 @@ const userLabelDefaultValues: UserLabelForm = {
userLabel: "", userLabel: "",
}; };
type ExpandableCredentialRepresentation = {
key: string;
value: CredentialRepresentation[];
isExpanded: boolean;
};
const DisplayDialog: FunctionComponent<DisplayDialogProps> = ({ const DisplayDialog: FunctionComponent<DisplayDialogProps> = ({
titleKey, titleKey,
onClose, onClose,
@ -241,6 +246,9 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
const [userCredentials, setUserCredentials] = useState< const [userCredentials, setUserCredentials] = useState<
CredentialRepresentation[] CredentialRepresentation[]
>([]); >([]);
const [groupedUserCredentials, setGroupedUserCredentials] = useState<
ExpandableCredentialRepresentation[]
>([]);
const [selectedCredential, setSelectedCredential] = const [selectedCredential, setSelectedCredential] =
useState<CredentialRepresentation>({}); useState<CredentialRepresentation>({});
const [isResetPassword, setIsResetPassword] = useState(false); const [isResetPassword, setIsResetPassword] = useState(false);
@ -256,6 +264,23 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
() => adminClient.users.getCredentials({ id: user.id! }), () => adminClient.users.getCredentials({ id: user.id! }),
(credentials) => { (credentials) => {
setUserCredentials(credentials); setUserCredentials(credentials);
const groupedCredentials = credentials.reduce((r, a) => {
r[a.type!] = r[a.type!] || [];
r[a.type!].push(a);
return r;
}, Object.create(null));
const groupedCredentialsArray = Object.keys(groupedCredentials).map(
(key) => ({ key, value: groupedCredentials[key] })
);
setGroupedUserCredentials(
groupedCredentialsArray.map((groupedCredential) => ({
...groupedCredential,
isExpanded: false,
}))
);
}, },
[key] [key]
); );
@ -671,7 +696,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
<> <>
<Button <Button
key={`confirmSaveBtn-table-${user.id}`} key={`confirmSaveBtn-table-${user.id}`}
className="setPasswordBtn-table" className="kc-setPasswordBtn-tbl"
data-testid="setPasswordBtn-table" data-testid="setPasswordBtn-table"
variant="primary" variant="primary"
form="userCredentials-form" form="userCredentials-form"
@ -684,7 +709,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
<Divider /> <Divider />
</> </>
)} )}
{userCredentials.length !== 0 ? ( {groupedUserCredentials.length !== 0 ? (
<> <>
{user.email && ( {user.email && (
<Button <Button
@ -712,161 +737,330 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
<Th /> <Th />
</Tr> </Tr>
</Thead> </Thead>
<Tbody> {groupedUserCredentials.map((groupedCredential, rowIndex) => (
{userCredentials.map((credential) => ( <Fragment key={`table-${groupedCredential.key}`}>
<Tr key={`table-${credential.id}`}> <Tr>
<> {groupedCredential.value.length > 1 ? (
<Td <Td
draggableRow={{ className="kc-expandRow-btn"
id: `draggable-row-${credential.id}`, expand={{
rowIndex,
isExpanded: groupedCredential.isExpanded,
onToggle: (_, rowIndex) => {
const rows = groupedUserCredentials.map(
(credential, index) =>
index === rowIndex
? {
...credential,
isExpanded: !credential.isExpanded,
}
: credential
);
setGroupedUserCredentials(rows);
},
}} }}
/> />
<Td ) : (
key={`table-item-${credential.id}`} <Td />
dataLabel={`columns-${credential.id}`} )}
> <Td
{credential.type?.charAt(0).toUpperCase()! + key={`table-item-${groupedCredential.key}`}
credential.type?.slice(1)} dataLabel={`columns-${groupedCredential.key}`}
</Td> className="kc-notExpandableRow-credentialType"
<Td> >
<FormAccess isHorizontal role="view-users"> {groupedCredential.key.charAt(0).toUpperCase()! +
<FormGroup groupedCredential.key.slice(1)}
fieldId="kc-userLabel" </Td>
className="kc-userLabel-row" {groupedCredential.value.length <= 1 &&
groupedCredential.value.map((credential) => (
<>
<Td>
<FormAccess
isHorizontal
role="view-users"
className="kc-form-userLabel"
>
<FormGroup
fieldId="kc-userLabel"
className="kc-userLabel-row"
>
<div className="kc-form-group-userLabel">
{isUserLabelEdit?.status &&
isUserLabelEdit.rowKey === credential.id ? (
<>
<TextInput
name="userLabel"
ref={register1()}
type="text"
className="kc-userLabel"
aria-label={t("userLabel")}
data-testid="user-label-fld"
/>
<div className="kc-userLabel-actionBtns">
<Button
key={`editUserLabel-accept-${credential.id}`}
variant="link"
className="kc-editUserLabel-acceptBtn"
onClick={() => {
handleSubmit1(saveUserLabel)();
setIsUserLabelEdit({
status: false,
rowKey: credential.id!,
});
}}
data-testid="editUserLabel-acceptBtn"
icon={<CheckIcon />}
/>
<Button
key={`editUserLabel-cancel-${credential.id}`}
variant="link"
className="kc-editUserLabel-cancelBtn"
onClick={() =>
setIsUserLabelEdit({
status: false,
rowKey: credential.id!,
})
}
data-testid="editUserLabel-cancelBtn"
icon={<TimesIcon />}
/>
</div>
</>
) : (
<>
{credential.userLabel ?? ""}
<Button
key={`editUserLabel-${credential.id}`}
variant="link"
className="kc-editUserLabel-btn"
onClick={() => {
setEditedUserCredential(credential);
setIsUserLabelEdit({
status: true,
rowKey: credential.id!,
});
}}
data-testid="editUserLabelBtn"
icon={<PencilAltIcon />}
/>
</>
)}
</div>
</FormGroup>
</FormAccess>
</Td>
<Td>
<Button
className="kc-showData-btn"
variant="link"
data-testid="showDataBtn"
onClick={() => {
setShowData(true);
setSelectedCredential(credential);
}}
>
{t("showDataBtn")}
</Button>
</Td>
{credential.type === "password" ? (
<Td>
<Button
variant="secondary"
data-testid="resetPasswordBtn"
onClick={resetPassword}
>
{t("resetPasswordBtn")}
</Button>
</Td>
) : (
<Td />
)}
<Td>
<Dropdown
isPlain
position={DropdownPosition.right}
toggle={
<KebabToggle
onToggle={(status) =>
setKebabOpen({
status,
rowKey: credential.id!,
})
}
/>
}
isOpen={
kebabOpen.status &&
kebabOpen.rowKey === credential.id
}
onSelect={() => {
setSelectedCredential(credential);
}}
dropdownItems={[
<DropdownItem
key={`delete-dropdown-item-${credential.id}`}
data-testid="deleteDropdownItem"
component="button"
onClick={() => {
toggleDeleteDialog();
setKebabOpen({
status: false,
rowKey: credential.id!,
});
}}
>
{t("deleteBtn")}
</DropdownItem>,
]}
/>
</Td>
</>
))}
</Tr>
{groupedCredential.isExpanded &&
groupedCredential.value.map((credential) => (
<Tr key={`child-key-${credential.id}`}>
<Td />
<Td
key={`child-item-${credential.id}`}
dataLabel={`child-columns-${credential.id}`}
className="kc-expandableRow-credentialType"
>
{credential.type!.charAt(0).toUpperCase()! +
credential.type!.slice(1)}
</Td>
<Td>
<FormAccess
isHorizontal
role="view-users"
className="kc-form-userLabel"
> >
<div className="kc-form-group-userLabel"> <FormGroup
{isUserLabelEdit?.status && fieldId="kc-userLabel"
isUserLabelEdit.rowKey === credential.id ? ( className="kc-userLabel-row"
<> >
<TextInput <div className="kc-form-group-userLabel">
name="userLabel" {isUserLabelEdit?.status &&
ref={register1()} isUserLabelEdit.rowKey === credential.id ? (
type="text" <>
className="kc-userLabel" <TextInput
aria-label={t("userLabel")} name="userLabel"
data-testid="user-label-fld" ref={register1()}
/> type="text"
<div className="kc-userLabel-actionBtns"> className="kc-userLabel"
aria-label={t("userLabel")}
data-testid="user-label-fld"
/>
<div className="kc-userLabel-actionBtns">
<Button
key={`editUserLabel-accept-${credential.id}`}
variant="link"
className="kc-editUserLabel-acceptBtn"
onClick={() => {
handleSubmit1(saveUserLabel)();
setIsUserLabelEdit({
status: false,
rowKey: credential.id!,
});
}}
data-testid="editUserLabel-acceptBtn"
icon={<CheckIcon />}
/>
<Button
key={`editUserLabel-cancel-${credential.id}`}
variant="link"
className="kc-editUserLabel-cancelBtn"
onClick={() =>
setIsUserLabelEdit({
status: false,
rowKey: credential.id!,
})
}
data-testid="editUserLabel-cancelBtn"
icon={<TimesIcon />}
/>
</div>
</>
) : (
<>
{credential.userLabel ?? ""}
<Button <Button
key={`editUserLabel-accept-${credential.id}`} key={`editUserLabel-${credential.id}`}
variant="link" variant="link"
className="kc-editUserLabel-acceptBtn" className="kc-editUserLabel-btn"
onClick={() => { onClick={() => {
handleSubmit1(saveUserLabel)(); setEditedUserCredential(credential);
setIsUserLabelEdit({ setIsUserLabelEdit({
status: false, status: true,
rowKey: credential.id!, rowKey: credential.id!,
}); });
}} }}
data-testid="editUserLabel-acceptBtn" data-testid="editUserLabelBtn"
icon={<CheckIcon />} icon={<PencilAltIcon />}
/> />
<Button </>
key={`editUserLabel-cancel-${credential.id}`} )}
variant="link" </div>
className="kc-editUserLabel-cancelBtn" </FormGroup>
onClick={() => </FormAccess>
setIsUserLabelEdit({ </Td>
status: false,
rowKey: credential.id!,
})
}
data-testid="editUserLabel-cancelBtn"
icon={<TimesIcon />}
/>
</div>
</>
) : (
<>
{credential.userLabel ?? ""}
<Button
key={`editUserLabel-${credential.id}`}
variant="link"
className="kc-editUserLabel-btn"
onClick={() => {
setEditedUserCredential(credential);
setIsUserLabelEdit({
status: true,
rowKey: credential.id!,
});
}}
data-testid="editUserLabelBtn"
icon={<PencilAltIcon />}
/>
</>
)}
</div>
</FormGroup>
</FormAccess>
</Td>
<Td>
<Button
className="kc-showData-btn"
variant="link"
data-testid="showDataBtn"
onClick={() => {
setShowData(true);
setSelectedCredential(credential);
}}
>
{t("showDataBtn")}
</Button>
</Td>
{credential.type === "password" ? (
<Td> <Td>
<Button <Button
variant="secondary" className="kc-showData-btn"
data-testid="resetPasswordBtn" variant="link"
onClick={resetPassword} data-testid="showDataBtn"
onClick={() => {
setShowData(true);
setSelectedCredential(credential);
}}
> >
{t("resetPasswordBtn")} {t("showDataBtn")}
</Button> </Button>
</Td> </Td>
) : (
<Td /> <Td />
)} <Td>
<Td> <Dropdown
<Dropdown isPlain
isPlain position={DropdownPosition.right}
position={DropdownPosition.right} toggle={
toggle={ <KebabToggle
<KebabToggle onToggle={(status) =>
onToggle={(status) => setKebabOpen({
setKebabOpen({ status,
status, rowKey: credential.id!,
rowKey: credential.id!, })
}) }
} />
/> }
} isOpen={
isOpen={ kebabOpen.status &&
kebabOpen.status && kebabOpen.rowKey === credential.id kebabOpen.rowKey === credential.id
} }
onSelect={() => { onSelect={() => {
setSelectedCredential(credential); setSelectedCredential(credential);
}} }}
dropdownItems={[ dropdownItems={[
<DropdownItem <DropdownItem
key={`delete-dropdown-item-${credential.id}`} key={`delete-dropdown-item-${credential.id}`}
data-testid="deleteDropdownItem" data-testid="deleteDropdownItem"
component="button" component="button"
onClick={() => { onClick={() => {
toggleDeleteDialog(); toggleDeleteDialog();
setKebabOpen({ setKebabOpen({
status: false, status: false,
rowKey: credential.id!, rowKey: credential.id!,
}); });
}} }}
> >
{t("deleteBtn")} {t("deleteBtn")}
</DropdownItem>, </DropdownItem>,
]} ]}
/> />
</Td> </Td>
</> </Tr>
</Tr> ))}
))} </Fragment>
</Tbody> ))}
</TableComposable> </TableComposable>
</> </>
) : ( ) : (

View file

@ -0,0 +1,69 @@
.kc-edit-icon {
color: var(--pf-global--Color--200);
margin-left: 5px;
}
.kc-showData-btn {
padding-left: 0;
}
.kc-userLabel-row {
display: inline-block !important;
width: 100%;
}
.kc-form-group-userLabel, .kc-userLabel-actionBtns {
display: flex;
}
.kc-editUserLabel-btn, .kc-editUserLabel-cancelBtn {
color: var(--pf-global--Color--200) !important;
}
.kc-editUserLabel-btn {
padding-top: 0px;
}
.kc-editUserLabel-btn:hover {
filter: brightness(55%);
}
.kc-editUserLabel-acceptBtn {
padding-right: 8px;
}
.kc-editUserLabel-cancelBtn {
padding-left: 8px !important;
}
.pf-c-table.pf-m-compact tr:not(.pf-c-table__expandable-row)>:last-child {
overflow-wrap: anywhere;
}
.kc-setPasswordBtn-tbl {
margin: 25px 0 25px 25px;
}
.kc-form-userLabel {
max-height: 0px;
margin-bottom: 0px;
padding-bottom: 15px;;
}
.kc-notExpandableRow-credentialType {
padding: 15px 0px 15px 15px !important;
}
.kc-expandableRow-credentialType {
padding-left: 15px !important;
}
.kc-expandRow-btn {
vertical-align: middle;
}
.kc-temporaryPassword {
margin: 6px 0 10px 35px;
}

View file

@ -119,61 +119,6 @@ article.pf-c-card.pf-m-flat.kc-available-idps > div > div > h1 {
text-align: center; text-align: center;
} }
.kc-temporaryPassword {
margin: 6px 0 10px 35px;
}
.keycloak__user-credentials__reset-form { .keycloak__user-credentials__reset-form {
--pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 13rem; --pf-c-form--m-horizontal__group-label--md--GridColumnWidth: 13rem;
} }
.kc-edit-icon {
color: var(--pf-global--Color--200);
margin-left: 5px;
}
.kc-showData-btn {
padding-left: 0;
}
.kc-userLabel-row {
display: inline-block !important;
width: 50%;
}
.kc-form-group-userLabel, .kc-userLabel-actionBtns {
display: flex;
}
.kc-editUserLabel-btn, .kc-editUserLabel-cancelBtn {
color: var(--pf-global--Color--200) !important;
}
.kc-editUserLabel-btn {
padding-top: 0px;
}
.kc-editUserLabel-btn:hover {
filter: brightness(55%);
}
.kc-editUserLabel-acceptBtn {
padding-right: 8px;
}
.kc-editUserLabel-cancelBtn {
padding-left: 8px !important;
}
.pf-c-table.pf-m-compact tr:not(.pf-c-table__expandable-row)>:last-child {
overflow-wrap: anywhere;
}
.setPasswordBtn-table {
margin: 25px 0 25px 25px;
}
.resetCredentialBtn-header {
margin: 10px 25px 10px 0;
float: right;
}