Fix various linting issues for Account Console

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2024-06-03 09:28:15 +02:00 committed by Hynek Mlnařík
parent 813b9f2f41
commit aa86a0b94c
10 changed files with 195 additions and 154 deletions

View file

@ -12,7 +12,11 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { LinkIcon, UnlinkIcon } from "@patternfly/react-icons"; import { LinkIcon, UnlinkIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { IconMapper, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; import {
IconMapper,
useAlerts,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { linkAccount, unLinkAccount } from "../api/methods"; import { linkAccount, unLinkAccount } from "../api/methods";
import { LinkedAccountRepresentation } from "../api/representations"; import { LinkedAccountRepresentation } from "../api/representations";

View file

@ -23,7 +23,11 @@ import {
} from "@patternfly/react-icons"; } from "@patternfly/react-icons";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; import {
ContinueCancelModal,
useAlerts,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { deleteSession, getDevices } from "../api/methods"; import { deleteSession, getDevices } from "../api/methods";
import { import {
ClientRepresentation, ClientRepresentation,

View file

@ -3,7 +3,13 @@ import { BaseEnvironment } from "@keycloak/keycloak-ui-shared/dist/context/envir
import { CallOptions } from "./api/methods"; import { CallOptions } from "./api/methods";
import { Links, parseLinks } from "./api/parse-links"; import { Links, parseLinks } from "./api/parse-links";
import { parseResponse } from "./api/parse-response"; import { parseResponse } from "./api/parse-response";
import { Permission, Resource, Scope, CredentialsIssuer, SupportedCredentialConfiguration } from "./api/representations"; import {
Permission,
Resource,
Scope,
CredentialsIssuer,
SupportedCredentialConfiguration,
} from "./api/representations";
import { request } from "./api/request"; import { request } from "./api/request";
import { joinPath } from "./utils/joinPath"; import { joinPath } from "./utils/joinPath";
@ -70,16 +76,20 @@ function checkResponse<T>(response: T) {
return response; return response;
} }
export async function getIssuer(context: KeycloakContext<BaseEnvironment>) {
export async function getIssuer(
context: KeycloakContext<BaseEnvironment>
) {
const response = await request( const response = await request(
"/realms/" + context.environment.realm + "/.well-known/openid-credential-issuer", "/realms/" +
context.environment.realm +
"/.well-known/openid-credential-issuer",
context, context,
{}, {},
new URL( new URL(
joinPath(context.environment.authUrl + "/realms/" + context.environment.realm + "/.well-known/openid-credential-issuer"), joinPath(
context.environment.authUrl +
"/realms/" +
context.environment.realm +
"/.well-known/openid-credential-issuer",
),
), ),
); );
return parseResponse<CredentialsIssuer>(response); return parseResponse<CredentialsIssuer>(response);
@ -87,16 +97,26 @@ export async function getIssuer(
export async function requestVCOffer( export async function requestVCOffer(
context: KeycloakContext<BaseEnvironment>, context: KeycloakContext<BaseEnvironment>,
supportedCredentialConfiguration:SupportedCredentialConfiguration, supportedCredentialConfiguration: SupportedCredentialConfiguration,
credentialsIssuer:CredentialsIssuer credentialsIssuer: CredentialsIssuer,
) { ) {
const response = await request( const response = await request(
"/protocol/oid4vc/credential-offer-uri", "/protocol/oid4vc/credential-offer-uri",
context, context,
{ searchParams: {"credential_configuration_id": supportedCredentialConfiguration.id, "type": "qr-code", "width": "500", "height": "500"} }, {
searchParams: {
credential_configuration_id: supportedCredentialConfiguration.id,
type: "qr-code",
width: "500",
height: "500",
},
},
new URL( new URL(
joinPath(credentialsIssuer.credential_issuer + "/protocol/oid4vc/credential-offer-uri"), joinPath(
credentialsIssuer.credential_issuer +
"/protocol/oid4vc/credential-offer-uri",
),
), ),
); );
return response.blob() return response.blob();
} }

View file

@ -211,6 +211,9 @@ export interface SupportedCredentialConfiguration {
export interface CredentialsIssuer { export interface CredentialsIssuer {
credential_issuer: string; credential_issuer: string;
credential_endpoint: string; credential_endpoint: string;
authorization_servers: string[]; authorization_servers: string[];
credential_configurations_supported: Record<string, SupportedCredentialConfiguration> credential_configurations_supported: Record<
} string,
SupportedCredentialConfiguration
>;
}

View file

@ -37,10 +37,10 @@ export async function request(
path: string, path: string,
{ environment, keycloak }: KeycloakContext<BaseEnvironment>, { environment, keycloak }: KeycloakContext<BaseEnvironment>,
opts: RequestOptions = {}, opts: RequestOptions = {},
fullUrl?: URL fullUrl?: URL,
) { ) {
if (typeof fullUrl === 'undefined') { if (typeof fullUrl === "undefined") {
fullUrl = url(environment, path) fullUrl = url(environment, path);
} }
return _request(fullUrl, { return _request(fullUrl, {
...opts, ...opts,

View file

@ -22,7 +22,11 @@ import {
} from "@patternfly/react-icons"; } from "@patternfly/react-icons";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; import {
ContinueCancelModal,
useAlerts,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { deleteConsent, getApplications } from "../api/methods"; import { deleteConsent, getApplications } from "../api/methods";
import { ClientRepresentation } from "../api/representations"; import { ClientRepresentation } from "../api/representations";
import { Page } from "../components/page/Page"; import { Page } from "../components/page/Page";

View file

@ -1,132 +1,136 @@
import { useEnvironment } from "@keycloak/keycloak-ui-shared";
import { import {
Select, ActionList,
SelectList, ActionListItem,
SelectOption, List,
PageSectionVariants, ListItem,
PageSection, MenuToggle,
ActionList, MenuToggleElement,
ActionListItem, PageSection,
List, PageSectionVariants,
ListItem, Select,
MenuToggleElement, SelectList,
MenuToggle SelectOption,
} from '@patternfly/react-core'; } from "@patternfly/react-core";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared";
import { usePromise } from "../utils/usePromise";
import { Page } from "../components/page/Page";
import { CredentialsIssuer } from "../api/representations";
import { getIssuer, requestVCOffer } from "../api"; import { getIssuer, requestVCOffer } from "../api";
import { CredentialsIssuer } from "../api/representations";
import { Page } from "../components/page/Page";
import { usePromise } from "../utils/usePromise";
export const Oid4Vci = () => { export const Oid4Vci = () => {
const context = useEnvironment(); const context = useEnvironment();
const { t } = useTranslation();
const initialSelected = t('verifiableCredentialsSelectionDefault') const { t } = useTranslation();
const [selected, setSelected] = useState<string>(initialSelected); const initialSelected = t("verifiableCredentialsSelectionDefault");
const [qrCode, setQrCode] = useState<string>("")
const [isOpen, setIsOpen] = useState<boolean>(false)
const [offerQRVisible, setOfferQRVisible] = useState<boolean>(false)
const [credentialsIssuer, setCredentialsIssuer] = useState<CredentialsIssuer>()
usePromise(() => getIssuer(context), setCredentialsIssuer); const [selected, setSelected] = useState<string>(initialSelected);
const [qrCode, setQrCode] = useState<string>("");
const [isOpen, setIsOpen] = useState<boolean>(false);
const [offerQRVisible, setOfferQRVisible] = useState<boolean>(false);
const [credentialsIssuer, setCredentialsIssuer] =
useState<CredentialsIssuer>();
const selectOptions = useMemo( usePromise(() => getIssuer(context), setCredentialsIssuer);
() => {
if(typeof credentialsIssuer !== 'undefined') {
return credentialsIssuer.credential_configurations_supported
}
return {}
},
[credentialsIssuer],
)
const dropdownItems = useMemo( const selectOptions = useMemo(() => {
() => { if (typeof credentialsIssuer !== "undefined") {
if (typeof selectOptions !== 'undefined') { return credentialsIssuer.credential_configurations_supported;
return Array.from(Object.keys(selectOptions)) }
} return {};
return [] }, [credentialsIssuer]);
},
[selectOptions],
)
useEffect(() => { const dropdownItems = useMemo(() => {
if(initialSelected !== selected && credentialsIssuer !== undefined){ if (typeof selectOptions !== "undefined") {
requestVCOffer(context, selectOptions[selected], credentialsIssuer) return Array.from(Object.keys(selectOptions));
.then((blob) => { }
var reader = new FileReader(); return [];
reader.readAsDataURL(blob) }, [selectOptions]);
reader.onloadend = function() {
let result = reader.result useEffect(() => {
if (typeof result === "string") { if (initialSelected !== selected && credentialsIssuer !== undefined) {
setQrCode(result); requestVCOffer(context, selectOptions[selected], credentialsIssuer).then(
setOfferQRVisible(true); (blob) => {
setIsOpen(false); const reader = new FileReader();
} reader.readAsDataURL(blob);
reader.onloadend = function () {
const result = reader.result;
if (typeof result === "string") {
setQrCode(result);
setOfferQRVisible(true);
setIsOpen(false);
} }
}) };
} },
}, [selected]);
const onToggleClick = () => {
setIsOpen(!isOpen);
};
const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
data-testid="menu-toggle"
>
{selected}
</MenuToggle>
); );
}
}, [selected]);
return ( const onToggleClick = () => {
<Page title={t('verifiableCredentialsTitle')} description={t('verifiableCredentialsDescription')}> setIsOpen(!isOpen);
<PageSection isFilled variant={PageSectionVariants.light}> };
<List isPlain>
<ListItem> const toggle = (toggleRef: React.Ref<MenuToggleElement>) => (
<Select <MenuToggle
data-testid="credential-select" ref={toggleRef}
onOpenChange={(isOpen) => setIsOpen(isOpen)} onClick={onToggleClick}
onSelect={(_event, val) => setSelected(val as string)} isExpanded={isOpen}
isOpen={isOpen} data-testid="menu-toggle"
selected={selected} >
toggle={toggle} {selected}
shouldFocusToggleOnSelect={true} </MenuToggle>
> );
<SelectList>
{dropdownItems.map((option, index) => ( return (
<SelectOption <Page
value={option} title={t("verifiableCredentialsTitle")}
data-testid='select-${option}' description={t("verifiableCredentialsDescription")}
> >
<PageSection isFilled variant={PageSectionVariants.light}>
<List isPlain>
<ListItem>
<Select
data-testid="credential-select"
onOpenChange={(isOpen) => setIsOpen(isOpen)}
onSelect={(_event, val) => setSelected(val as string)}
isOpen={isOpen}
selected={selected}
toggle={toggle}
shouldFocusToggleOnSelect={true}
>
<SelectList>
{dropdownItems.map((option) => (
<SelectOption
key={option}
value={option}
data-testid="select-${option}"
>
{option} {option}
</SelectOption> </SelectOption>
))} ))}
</SelectList> </SelectList>
</Select> </Select>
</ListItem> </ListItem>
<ListItem> <ListItem>
<ActionList> <ActionList>
{ offerQRVisible && {offerQRVisible && (
<ActionListItem> <ActionListItem>
<img width='500' height='500' src={`${qrCode}`} data-testid="qr-code"/> <img
</ActionListItem> width="500"
} height="500"
</ActionList> src={`${qrCode}`}
</ListItem> data-testid="qr-code"
</List> />
</PageSection> </ActionListItem>
</Page> )}
); </ActionList>
</ListItem>
</List>
</PageSection>
</Page>
);
}; };
export default Oid4Vci;
export default Oid4Vci;

View file

@ -32,7 +32,11 @@ import {
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ContinueCancelModal, useAlerts, useEnvironment } from "@keycloak/keycloak-ui-shared"; import {
ContinueCancelModal,
useAlerts,
useEnvironment,
} from "@keycloak/keycloak-ui-shared";
import { fetchPermission, fetchResources, updatePermissions } from "../api"; import { fetchPermission, fetchResources, updatePermissions } from "../api";
import { getPermissionRequests } from "../api/methods"; import { getPermissionRequests } from "../api/methods";
import { Links } from "../api/parse-links"; import { Links } from "../api/parse-links";

View file

@ -61,7 +61,7 @@ export const PersonalInfoRoute: IndexRouteObject = {
export const Oid4VciRoute: RouteObject = { export const Oid4VciRoute: RouteObject = {
path: "oid4vci", path: "oid4vci",
element: <Oid4Vci />, element: <Oid4Vci />,
} };
export const RootRoute: RouteObject = { export const RootRoute: RouteObject = {
path: decodeURIComponent(new URL(environment.baseUrl).pathname), path: decodeURIComponent(new URL(environment.baseUrl).pathname),

View file

@ -1,19 +1,17 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { login } from "../login"; import { login } from "../login";
const realm = "verifiable-credentials"
test.describe("Verifiable Credentials page", () => { test.describe("Verifiable Credentials page", () => {
test("Get offer for test-credential.", async ({ page }) => {
test("Get offer for test-credential.", async ({ page }) => { await login(page, "test-user", "test");
await expect(page.getByTestId("qr-code")).toBeHidden();
await login(page, "test-user", "test") await page.getByTestId("oid4vci").click();
await expect(page.getByTestId("qr-code")).toBeHidden await page.getByTestId("credential-select").click();
await page.getByTestId("oid4vci").click await expect(
await page.getByTestId("credential-select").click page.getByTestId("select-verifiable-credential"),
await expect(page.getByTestId("select-verifiable-credential")).toBeVisible ).toBeVisible();
await expect(page.getByTestId("select-natural-person")).toBeVisible await expect(page.getByTestId("select-natural-person")).toBeVisible();
await page.getByTestId("select-natural-person").click await page.getByTestId("select-natural-person").click();
await expect(page.getByTestId("qr-code")).toBeVisible await expect(page.getByTestId("qr-code")).toBeVisible();
}) });
}) });