diff --git a/apps/account-ui/public/locales/en/translation.json b/apps/account-ui/public/locales/en/translation.json index 3ad6b63c32..33ebf74370 100644 --- a/apps/account-ui/public/locales/en/translation.json +++ b/apps/account-ui/public/locales/en/translation.json @@ -9,10 +9,12 @@ "applicationsIntroMessage": "Track and manage your app permission to access your account", "applicationType": "Application type", "avatar": "Avatar", + "basic-authentication": "Basic authentication", "cancel": "Cancel", "client": "Client", "clients": "Clients", "close": "Close", + "credentialCreatedAt": "<0>Created0> {{date}}.", "currentSession": "Current session", "description": "Description", "device-activity": "Device activity", @@ -40,7 +42,13 @@ "myResources": "My Resources", "name": "Name", "notInUse": "Not in use", + "notSetUp": "{{0}} is not set up.", "offlineAccess": "Offline access", + "otp-display-name": "Authenticator application", + "otp-help-text": "Enter a verification code from authenticator application.", + "password-display-name": "Password", + "password-help-text": "Sign in by entering your password.", + "password": "My password", "permissionRequest": "Permission requests - {{0}}", "permissionRequests": "Permission requests", "permissions": "Permissions", @@ -49,6 +57,8 @@ "privacyPolicy": "Privacy policy", "refreshPage": "Refresh the page", "removeButton": "Remove access", + "removeCred": "Remove {{0}}", + "removeCredAriaLabel": "Remove credential", "removeModalMessage": "This will remove the currently granted access permission for {{0}}. You will need to grant access again if you want to use this app.", "removeModalTitle": "Remove access", "requestor": "Requestor", @@ -60,6 +70,7 @@ "resourceSharedWith_one": "Resource is shared with <0>{{username}}0>", "resourceSharedWith_other": "Resource is shared with <0>{{username}}0> and <1>{{other}}1> other users", "resourceSharedWith_zero": "This resource is not shared.", + "setUpNew": "Set up {{0}}", "share": "Share", "sharedWithMe": "Shared with Me", "shareTheResource": "Share the resource - {{0}}", @@ -69,6 +80,7 @@ "signedInDevicesExplanation": "Sign out of any unfamiliar devices.", "signedOutSession": "Signed out {{0}}/{{1}}", "signingIn": "Signing in", + "signingInDescription": "Configure ways to sign in.", "signOut": "Sign out", "signOutAllDevices": "Sign out all devices", "signOutAllDevicesWarning": "This action will sign out all the devices that have signed in to your account, including the current device you are using.", @@ -76,12 +88,16 @@ "somethingWentWrongDescription": "Sorry, an unexpected error has occurred.", "started": "Started", "status": "Status", + "stopUsingCred": "Stop using {{0}}?", "termsOfService": "Terms of service", "thirdPartyApp": "Third-party", "tryAgain": "Try again", + "two-factor": "Two-factor authentication", "unknownOperatingSystem": "Unknown operating system", "unknownUser": "Anonymous", "unShare": "Unshare all", + "update": "Update", + "updateCredAriaLabel": "Update credential", "user": "User", "username": "Username", "usernamePlaceholder": "Username or email", diff --git a/apps/account-ui/src/account-security/SigningIn.tsx b/apps/account-ui/src/account-security/SigningIn.tsx index 678a966547..043f6717d5 100644 --- a/apps/account-ui/src/account-security/SigningIn.tsx +++ b/apps/account-ui/src/account-security/SigningIn.tsx @@ -1,5 +1,236 @@ -import { PageSection } from "@patternfly/react-core"; +import { + Button, + DataList, + DataListAction, + DataListCell, + DataListItem, + DataListItemCells, + DataListItemRow, + Dropdown, + DropdownItem, + EmptyState, + EmptyStateBody, + KebabToggle, + PageSection, + Spinner, + Split, + SplitItem, + Title, +} from "@patternfly/react-core"; +import { TFuncKey } from "i18next"; +import { useState, CSSProperties } from "react"; +import { useTranslation, Trans } from "react-i18next"; +import { deleteCredentials, getCredentials } from "../api/methods"; +import { + CredentialContainer, + CredentialMetadataRepresentation, + CredentialRepresentation, +} from "../api/representations"; +import { useAlerts } from "../components/alerts/Alerts"; +import { ContinueCancelModal } from "../components/continue-cancel/ContinueCancelModel"; +import useFormatter from "../components/format/format-date"; +import { Page } from "../components/page/Page"; +import { keycloak } from "../keycloak"; +import { usePromise } from "../utils/usePromise"; -const SigningIn = () => This is the signing in page.; +type MobileLinkProps = { + title: string; + onClick: () => void; +}; + +const MobileLink = ({ title, onClick }: MobileLinkProps) => { + const [open, setOpen] = useState(false); + return ( + <> + } + className="pf-u-display-none-on-lg" + isOpen={open} + dropdownItems={[ + + {title} + , + ]} + /> + + {title} + + > + ); +}; + +const SigningIn = () => { + const { t } = useTranslation(); + const { formatDate } = useFormatter(); + const { addAlert, addError } = useAlerts(); + const { login } = keycloak; + + const [credentials, setCredentials] = useState(); + const [key, setKey] = useState(1); + const refresh = () => setKey(key + 1); + + usePromise((signal) => getCredentials({ signal }), setCredentials, [key]); + + const credentialRowCells = ( + credMetadata: CredentialMetadataRepresentation + ) => { + const credential = credMetadata.credential; + const maxWidth = { "--pf-u-max-width--MaxWidth": "300px" } as CSSProperties; + const items = [ + + {credential.userLabel || t(credential.type as TFuncKey)} + , + ]; + + if (credential.createdDate) { + items.push( + + + + {{ date: formatDate(new Date(credential.createdDate)) }} + + + ); + } + return items; + }; + + const label = (credential: CredentialRepresentation) => + credential.userLabel || t(credential.type as TFuncKey); + + if (!credentials) { + return ; + } + + return ( + + + {credentials.map((container) => ( + + + {t(container.category as TFuncKey)} + + + + + + {t(container.displayName as TFuncKey)} + + + {t(container.helptext as TFuncKey)} + + {container.createAction && ( + + + + login({ + action: container.createAction, + }) + } + title={t("setUpNew", [ + t(container.displayName as TFuncKey), + ])} + /> + + + )} + + + + {container.userCredentialMetadatas.length === 0 && ( + + + , + + + {t("notSetUp", [ + t(container.displayName as TFuncKey), + ])} + + , + , + , + ]} + /> + + + )} + + {container.userCredentialMetadatas.map((meta) => ( + + + + {container.removeable ? ( + { + try { + await deleteCredentials(meta.credential); + addAlert("successRemovedMessage"); + refresh(); + } catch (error) { + addError("errorRemovedMessage", error); + } + }} + /> + ) : ( + { + if (container.updateAction) + login({ action: container.updateAction }); + }} + > + {t("update")} + + )} + , + ]} + /> + + + ))} + + + ))} + + + ); +}; export default SigningIn; diff --git a/apps/account-ui/src/api/methods.ts b/apps/account-ui/src/api/methods.ts index 9d7e0f4785..40a0126490 100644 --- a/apps/account-ui/src/api/methods.ts +++ b/apps/account-ui/src/api/methods.ts @@ -1,6 +1,8 @@ import { parseResponse } from "./parse-response"; import { ClientRepresentation, + CredentialContainer, + CredentialRepresentation, DeviceRepresentation, Permission, UserRepresentation, @@ -58,3 +60,16 @@ export async function deleteSession(id?: string) { method: "DELETE", }); } + +export async function getCredentials({ signal }: CallOptions) { + const response = await request("/credentials", { + signal, + }); + return parseResponse(response); +} + +export async function deleteCredentials(credential: CredentialRepresentation) { + return request("/credentials/" + credential.id, { + method: "DELETE", + }); +} diff --git a/apps/account-ui/src/components/format/format-date.ts b/apps/account-ui/src/components/format/format-date.ts new file mode 100644 index 0000000000..1193b7409c --- /dev/null +++ b/apps/account-ui/src/components/format/format-date.ts @@ -0,0 +1,30 @@ +const DATE_AND_TIME_FORMAT: Intl.DateTimeFormatOptions = { + dateStyle: "long", + timeStyle: "short", +}; + +const TIME_FORMAT: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", +}; + +//todo use user local +export default function useFormatter() { + return { + formatDate: function ( + date: Date, + options: Intl.DateTimeFormatOptions | undefined = DATE_AND_TIME_FORMAT + ) { + return date.toLocaleString("en", options); + }, + formatTime: function ( + time: number, + options: Intl.DateTimeFormatOptions | undefined = TIME_FORMAT + ) { + return new Intl.DateTimeFormat("en", options).format(time); + }, + }; +} diff --git a/apps/account-ui/src/main.tsx b/apps/account-ui/src/main.tsx index 93e0677d1c..5c418fd6aa 100644 --- a/apps/account-ui/src/main.tsx +++ b/apps/account-ui/src/main.tsx @@ -1,4 +1,5 @@ import "@patternfly/react-core/dist/styles/base.css"; +import "@patternfly/patternfly/patternfly-addons.css"; import { StrictMode } from "react"; import { render } from "react-dom";