Add credential table (#3710)

This commit is contained in:
Erik Jan de Wit 2022-11-07 15:02:53 -05:00 committed by GitHub
parent 25c66e6901
commit 2ae520fb8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 157 additions and 102 deletions

View file

@ -46,7 +46,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^19.0.3", "@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
"@patternfly/patternfly": "^4.219.2", "@patternfly/patternfly": "^4.219.2",
"@patternfly/react-code-editor": "^4.82.55", "@patternfly/react-code-editor": "^4.82.55",
"@patternfly/react-core": "^4.258.3", "@patternfly/react-core": "^4.258.3",

View file

@ -150,6 +150,7 @@
"type": "Type", "type": "Type",
"userLabel": "User label", "userLabel": "User label",
"data": "Data", "data": "Data",
"providedBy": "Provided by",
"passwordDataTitle": "Password data", "passwordDataTitle": "Password data",
"updateCredentialUserLabelSuccess": "The user label has been changed successfully.", "updateCredentialUserLabelSuccess": "The user label has been changed successfully.",
"updateCredentialUserLabelError": "Error changing user label: {{error}}", "updateCredentialUserLabelError": "Error changing user label: {{error}}",

View file

@ -94,6 +94,10 @@ export const ResourcesPolicySelect = ({
]) ])
) )
.flat() .flat()
.filter(
(r): r is PolicyRepresentation | ResourceRepresentation =>
typeof r !== "string"
)
.map(convert) .map(convert)
.filter( .filter(
({ id }, index, self) => ({ id }, index, self) =>

View file

@ -134,24 +134,13 @@ export default function NewAttributeSettings() {
flatten<any, any>({ permissions, selector, required }, { safe: true }) flatten<any, any>({ permissions, selector, required }, { safe: true })
).map(([key, value]) => form.setValue(key, value)); ).map(([key, value]) => form.setValue(key, value));
form.setValue("annotations", convert(annotations)); form.setValue("annotations", convert(annotations));
form.setValue("validations", convert(validations)); form.setValue("validations", validations);
form.setValue("isRequired", required !== undefined); form.setValue("isRequired", required !== undefined);
}, },
[] []
); );
const save = async (profileConfig: UserProfileAttributeType) => { const save = async (profileConfig: UserProfileAttributeType) => {
const validations = profileConfig.validations?.reduce(
(prevValidations: any, currentValidations: any) => {
prevValidations[currentValidations.key] =
currentValidations.value?.length === 0
? {}
: currentValidations.value;
return prevValidations;
},
{}
);
const annotations = (profileConfig.annotations! as KeyValueType[]).reduce( const annotations = (profileConfig.annotations! as KeyValueType[]).reduce(
(obj, item) => Object.assign(obj, { [item.key]: item.value }), (obj, item) => Object.assign(obj, { [item.key]: item.value }),
{} {}
@ -169,7 +158,6 @@ export default function NewAttributeSettings() {
...attribute, ...attribute,
name: attributeName, name: attributeName,
displayName: profileConfig.displayName!, displayName: profileConfig.displayName!,
validations,
selector: profileConfig.selector, selector: profileConfig.selector,
permissions: profileConfig.permissions!, permissions: profileConfig.permissions!,
annotations, annotations,
@ -188,7 +176,6 @@ export default function NewAttributeSettings() {
name: profileConfig.name, name: profileConfig.name,
displayName: profileConfig.displayName!, displayName: profileConfig.displayName!,
required: profileConfig.isRequired ? profileConfig.required : {}, required: profileConfig.isRequired ? profileConfig.required : {},
validations,
selector: profileConfig.selector, selector: profileConfig.selector,
permissions: profileConfig.permissions!, permissions: profileConfig.permissions!,
annotations, annotations,

View file

@ -37,6 +37,7 @@ import { CredentialRow } from "./user-credentials/CredentialRow";
import { toUpperCase } from "../util"; import { toUpperCase } from "../util";
import "./user-credentials.css"; import "./user-credentials.css";
import { FederatedCredentials } from "./user-credentials/FederatedCredentials";
type UserCredentialsProps = { type UserCredentialsProps = {
user: UserRepresentation; user: UserRepresentation;
@ -361,7 +362,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
<Divider /> <Divider />
</> </>
)} )}
{groupedUserCredentials.length !== 0 ? ( {groupedUserCredentials.length !== 0 && (
<> <>
{user.email && ( {user.email && (
<Button <Button
@ -374,10 +375,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
</Button> </Button>
)} )}
<PageSection variant={PageSectionVariants.light}> <PageSection variant={PageSectionVariants.light}>
<TableComposable <TableComposable variant={"compact"}>
aria-label="userCredentials-table"
variant={"compact"}
>
<Thead> <Thead>
<Tr className="kc-table-header"> <Tr className="kc-table-header">
<Th> <Th>
@ -490,26 +488,31 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
</TableComposable> </TableComposable>
</PageSection> </PageSection>
</> </>
) : (
<ListEmptyState
hasIcon={true}
message={t("noCredentials")}
instructions={t("noCredentialsText")}
primaryActionText={t("setPassword")}
onPrimaryAction={toggleModal}
secondaryActions={
user.email
? [
{
text: t("credentialResetBtn"),
onClick: toggleCredentialsResetModal,
type: ButtonVariant.link,
},
]
: undefined
}
/>
)} )}
{(user.federationLink || user.origin) && (
<FederatedCredentials user={user} onSetPassword={toggleModal} />
)}
{groupedUserCredentials.length === 0 &&
!(user.federationLink || user.origin) && (
<ListEmptyState
hasIcon
message={t("noCredentials")}
instructions={t("noCredentialsText")}
primaryActionText={t("setPassword")}
onPrimaryAction={toggleModal}
secondaryActions={
user.email
? [
{
text: t("credentialResetBtn"),
onClick: toggleCredentialsResetModal,
type: ButtonVariant.link,
},
]
: undefined
}
/>
)}
</> </>
); );
}; };

View file

@ -0,0 +1,112 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
PageSection,
PageSectionVariants,
} from "@patternfly/react-core";
import {
TableComposable,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";
import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { useAccess } from "../../context/access/Access";
import { toUserFederationLdap } from "../../user-federation/routes/UserFederationLdap";
import { useRealm } from "../../context/realm-context/RealmContext";
import { Link } from "react-router-dom";
type FederatedCredentialsProps = {
user: UserRepresentation;
onSetPassword: () => void;
};
export const FederatedCredentials = ({
user,
onSetPassword,
}: FederatedCredentialsProps) => {
const { t } = useTranslation("users");
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const [credentialTypes, setCredentialTypes] = useState<string[]>();
const [component, setComponent] = useState<ComponentRepresentation>();
const access = useAccess();
useFetch(
() =>
Promise.all([
adminClient.users.getUserStorageCredentialTypes({ id: user.id! }),
access.hasAccess("view-realm")
? adminClient.components.findOne({
id: (user.federationLink || user.origin)!,
})
: adminClient.userStorageProvider.name({
id: (user.federationLink || user.origin)!,
}),
]),
([credentialTypes, component]) => {
setCredentialTypes(credentialTypes);
setComponent(component);
},
[]
);
if (!credentialTypes || !component) {
return <KeycloakSpinner />;
}
return (
<PageSection variant={PageSectionVariants.light}>
<TableComposable variant={"compact"}>
<Thead>
<Tr>
<Th>{t("type")}</Th>
<Th>{t("providedBy")}</Th>
<Th />
</Tr>
</Thead>
<Tbody>
{credentialTypes.map((credential) => (
<Tr key={credential}>
<Td>
<b>{credential}</b>
</Td>
<Td>
<Button
variant="link"
isDisabled={!access.hasAccess("view-realm")}
component={(props) => (
<Link
{...props}
to={toUserFederationLdap({
id: component.id!,
realm,
})}
/>
)}
>
{component.name}
</Button>
</Td>
{credential === "password" && (
<Td modifier="fitContent">
<Button variant="secondary" onClick={onSetPassword}>
{t("setPassword")}
</Button>
</Td>
)}
</Tr>
))}
</Tbody>
</TableComposable>
</PageSection>
);
};

74
package-lock.json generated
View file

@ -140,7 +140,7 @@
}, },
"apps/admin-ui": { "apps/admin-ui": {
"dependencies": { "dependencies": {
"@keycloak/keycloak-admin-client": "^19.0.3", "@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
"@patternfly/patternfly": "^4.219.2", "@patternfly/patternfly": "^4.219.2",
"@patternfly/react-code-editor": "^4.82.55", "@patternfly/react-code-editor": "^4.82.55",
"@patternfly/react-core": "^4.258.3", "@patternfly/react-core": "^4.258.3",
@ -2953,13 +2953,13 @@
} }
}, },
"node_modules/@keycloak/keycloak-admin-client": { "node_modules/@keycloak/keycloak-admin-client": {
"version": "19.0.3", "version": "21.0.0-dev.3",
"license": "Apache-2.0", "resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz",
"integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
"camelize-ts": "^2.1.1", "camelize-ts": "^2.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"query-string": "^7.0.1",
"url-join": "^5.0.0", "url-join": "^5.0.0",
"url-template": "^3.0.0" "url-template": "^3.0.0"
}, },
@ -6822,6 +6822,7 @@
}, },
"node_modules/decode-uri-component": { "node_modules/decode-uri-component": {
"version": "0.2.0", "version": "0.2.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
@ -8354,13 +8355,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/filter-obj": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/find-cache-dir": { "node_modules/find-cache-dir": {
"version": "3.3.2", "version": "3.3.2",
"dev": true, "dev": true,
@ -11726,22 +11720,6 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/query-string": {
"version": "7.1.1",
"license": "MIT",
"dependencies": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/querystring": { "node_modules/querystring": {
"version": "0.2.0", "version": "0.2.0",
"dev": true, "dev": true,
@ -12856,13 +12834,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/split-on-first": {
"version": "1.1.0",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/split-string": { "node_modules/split-string": {
"version": "3.1.0", "version": "3.1.0",
"dev": true, "dev": true,
@ -13054,13 +13025,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"dev": true, "dev": true,
@ -16898,12 +16862,13 @@
} }
}, },
"@keycloak/keycloak-admin-client": { "@keycloak/keycloak-admin-client": {
"version": "19.0.3", "version": "21.0.0-dev.3",
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-21.0.0-dev.3.tgz",
"integrity": "sha512-wFvxZXrVGiHgd3OCab4YWAwcinykPYhuNlO8BokyP6XClKKL5qCQgNm+iWxgDB/njUdY3ovqDBTnY9KUKI3qWA==",
"requires": { "requires": {
"axios": "^0.27.2", "axios": "^0.27.2",
"camelize-ts": "^2.1.1", "camelize-ts": "^2.1.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"query-string": "^7.0.1",
"url-join": "^5.0.0", "url-join": "^5.0.0",
"url-template": "^3.0.0" "url-template": "^3.0.0"
} }
@ -18311,7 +18276,7 @@
"@babel/preset-env": "^7.19.4", "@babel/preset-env": "^7.19.4",
"@cypress/webpack-batteries-included-preprocessor": "^2.2.3", "@cypress/webpack-batteries-included-preprocessor": "^2.2.3",
"@cypress/webpack-preprocessor": "^5.15.3", "@cypress/webpack-preprocessor": "^5.15.3",
"@keycloak/keycloak-admin-client": "^19.0.3", "@keycloak/keycloak-admin-client": "^21.0.0-dev.3",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@patternfly/patternfly": "^4.219.2", "@patternfly/patternfly": "^4.219.2",
"@patternfly/react-code-editor": "^4.82.55", "@patternfly/react-code-editor": "^4.82.55",
@ -19725,7 +19690,8 @@
"dev": true "dev": true
}, },
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0" "version": "0.2.0",
"dev": true
}, },
"decompress": { "decompress": {
"version": "4.2.1", "version": "4.2.1",
@ -20742,9 +20708,6 @@
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }
}, },
"filter-obj": {
"version": "1.1.0"
},
"find-cache-dir": { "find-cache-dir": {
"version": "3.3.2", "version": "3.3.2",
"dev": true, "dev": true,
@ -22943,15 +22906,6 @@
"version": "6.5.3", "version": "6.5.3",
"dev": true "dev": true
}, },
"query-string": {
"version": "7.1.1",
"requires": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
}
},
"querystring": { "querystring": {
"version": "0.2.0", "version": "0.2.0",
"dev": true "dev": true
@ -23686,9 +23640,6 @@
"version": "1.4.8", "version": "1.4.8",
"dev": true "dev": true
}, },
"split-on-first": {
"version": "1.1.0"
},
"split-string": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"dev": true, "dev": true,
@ -23830,9 +23781,6 @@
"version": "1.0.1", "version": "1.0.1",
"dev": true "dev": true
}, },
"strict-uri-encode": {
"version": "2.0.0"
},
"string_decoder": { "string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"dev": true, "dev": true,