From d241f63a2200894fd926eb4e35e6697ee9569dd2 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Fri, 3 Feb 2023 12:56:55 +0100 Subject: [PATCH] Add applications page to account ui (#4254) --- .../public/locales/en/translation.json | 19 ++ apps/account-ui/src/api/methods.ts | 17 +- .../src/applications/Applications.tsx | 271 +++++++++++++++++- 3 files changed, 302 insertions(+), 5 deletions(-) diff --git a/apps/account-ui/public/locales/en/translation.json b/apps/account-ui/public/locales/en/translation.json index dcb8995f14..d69be9f75e 100644 --- a/apps/account-ui/public/locales/en/translation.json +++ b/apps/account-ui/public/locales/en/translation.json @@ -1,12 +1,18 @@ { "accept": "Accept", + "accessGrantedOn": "Access granted on", "accountSecurity": "Account security", "add": "Add", "application": "Application", + "applicationDetails": "Application details", "applications": "Applications", + "applicationsIntroMessage": "Track and manage your app permission to access your account", + "applicationType": "Application type", "avatar": "Avatar", "cancel": "Cancel", + "client": "Client", "close": "Close", + "description": "Description", "deviceActivity": "Device activity", "doDeny": "Deny", "done": "Done", @@ -16,16 +22,26 @@ "firstName": "First name", "fullName": "{{givenName}} {{familyName}}", "groups": "Groups", + "infoMessage": "By clicking Remove Access, you will remove granted permissions of this application. This application will no longer use your information.", + "internalApp": "Internal", + "inUse": "In use", "lastName": "Last name", "linkedAccounts": "Linked accounts", "logo": "Logo", "manageAccount": "Manage account", "myResources": "My Resources", + "name": "Name", + "notInUse": "Not in use", + "offlineAccess": "Offline access", "permissionRequest": "Permission requests - {{0}}", "permissionRequests": "Permission requests", "permissions": "Permissions", "personalInfo": "Personal info", "personalInfoDescription": "Manage your basic information", + "privacyPolicy": "Privacy policy", + "removeButton": "Remove access", + "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", "required": "Required", "resourceAlreadyShared": "Resource is already shared with this user.", @@ -44,6 +60,9 @@ "signOut": "Sign out", "somethingWentWrong": "Something went wrong", "somethingWentWrongDescription": "Sorry, an unexpected error has occurred.", + "status": "Status", + "termsOfService": "Terms of service", + "thirdPartyApp": "Third-party", "tryAgain": "Try again", "unknownUser": "Anonymous", "unShare": "Unshare all", diff --git a/apps/account-ui/src/api/methods.ts b/apps/account-ui/src/api/methods.ts index e3f3eb928d..6c5bf39060 100644 --- a/apps/account-ui/src/api/methods.ts +++ b/apps/account-ui/src/api/methods.ts @@ -1,5 +1,9 @@ import { parseResponse } from "./parse-response"; -import { Permission, UserRepresentation } from "./representations"; +import { + ClientRepresentation, + Permission, + UserRepresentation, +} from "./representations"; import { request } from "./request"; export type CallOptions = { @@ -29,3 +33,14 @@ export async function getPermissionRequests( return parseResponse(response); } + +export async function getApplications({ signal }: CallOptions = {}): Promise< + ClientRepresentation[] +> { + const response = await request("/applications", { signal }); + return parseResponse(response); +} + +export async function deleteConsent(id: string) { + return request(`/applications/${id}/consent`, { method: "DELETE" }); +} diff --git a/apps/account-ui/src/applications/Applications.tsx b/apps/account-ui/src/applications/Applications.tsx index c350045444..7b2f08e196 100644 --- a/apps/account-ui/src/applications/Applications.tsx +++ b/apps/account-ui/src/applications/Applications.tsx @@ -1,7 +1,270 @@ -import { PageSection } from "@patternfly/react-core"; +import { + Button, + DataList, + DataListCell, + DataListContent, + DataListItem, + DataListItemCells, + DataListItemRow, + DataListToggle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Grid, + GridItem, + Spinner, +} from "@patternfly/react-core"; +import { + CheckIcon, + ExternalLinkAltIcon, + InfoAltIcon, +} from "@patternfly/react-icons"; +import { TFuncKey } from "i18next"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { deleteConsent, getApplications } from "../api/methods"; +import { ClientRepresentation } from "../api/representations"; +import { useAlerts } from "../components/alerts/Alerts"; +import { ContinueCancelModal } from "../components/continue-cancel/ContinueCancelModel"; +import { Page } from "../components/page/Page"; +import { usePromise } from "../utils/usePromise"; -const Applications = () => ( - This is the applications page. -); +type Application = ClientRepresentation & { + open: boolean; +}; + +const Applications = () => { + const { t } = useTranslation(); + const { addAlert, addError } = useAlerts(); + + const [applications, setApplications] = useState(); + const [key, setKey] = useState(1); + const refresh = () => setKey(key + 1); + + usePromise( + (signal) => getApplications({ signal }), + (clients) => setApplications(clients.map((c) => ({ ...c, open: false }))), + [key] + ); + + const toggleOpen = (clientId: string) => { + setApplications([ + ...applications!.map((a) => + a.clientId === clientId ? { ...a, open: !a.open } : a + ), + ]); + }; + + const removeConsent = async (id: string) => { + try { + await deleteConsent(id); + refresh(); + addAlert("removeConsentSuccess"); + } catch (error) { + addError("removeConsentError", error); + } + }; + + if (!applications) { + return ; + } + + return ( + + + + + + + + + {t("name")} + , + + {t("applicationType")} + , + + {t("status")} + , + ]} + /> + + + {applications.map((application) => ( + + + toggleOpen(application.clientId)} + isExpanded={application.open} + id={`toggle-${application.clientId}`} + /> + + + , + + {application.userConsentRequired + ? t("thirdPartyApp") + : t("internalApp")} + {application.offlineAccess ? ", " + t("offlineAccess") : ""} + , + + {application.inUse ? t("inUse") : t("notInUse")} + , + ]} + /> + + + + + + {t("client")} + + {application.clientId} + + + {application.description && ( + + + {t("description")} + + + {application.description} + + + )} + {application.effectiveUrl && ( + + URL + + {application.effectiveUrl.split('"')} + + + )} + {application.consent && ( + <> + + Has access to + {application.consent.grantedScopes.map((scope) => ( + + {t(scope.name as TFuncKey)} + + ))} + + {application.tosUri && ( + + + {t("termsOfService")} + + + {application.tosUri} + + + )} + {application.policyUri && ( + + + {t("privacyPolicy")} + + + {application.policyUri} + + + )} + {application.logoUri && ( + + {t("logo")} + + + + + )} + + + {t("accessGrantedOn") + ": "} + + + {new Intl.DateTimeFormat("en", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }).format(application.consent.createdDate)} + + + + )} + + {(application.consent || application.offlineAccess) && ( + +
+ + removeConsent(application.clientId)} // required + /> + + + {t("infoMessage")} + +
+ )} +
+
+ ))} +
+
+ ); +}; export default Applications;