Add account-ui device activity page (#4256)
This commit is contained in:
parent
68fbc8db61
commit
ff1d7fcfc5
4 changed files with 301 additions and 4 deletions
|
@ -11,13 +11,19 @@
|
|||
"avatar": "Avatar",
|
||||
"cancel": "Cancel",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"close": "Close",
|
||||
"currentSession": "Current session",
|
||||
"description": "Description",
|
||||
"device-activity": "Device activity",
|
||||
"deviceActivity": "Device activity",
|
||||
"doDeny": "Deny",
|
||||
"done": "Done",
|
||||
"doSignOut": "Sign out",
|
||||
"edit": "Edit",
|
||||
"editTheResource": "Share the resource - {{0}}",
|
||||
"errorSignOutMessage": "Could not be signed out: {{error}}",
|
||||
"expires": "Expires",
|
||||
"filterByName": "Filter By Name ...",
|
||||
"firstName": "First name",
|
||||
"fullName": "{{givenName}} {{familyName}}",
|
||||
|
@ -25,6 +31,8 @@
|
|||
"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",
|
||||
"ipAddress": "IP address",
|
||||
"lastAccessedOn": "Last accessed",
|
||||
"lastName": "Last name",
|
||||
"linkedAccounts": "Linked accounts",
|
||||
"logo": "Logo",
|
||||
|
@ -39,6 +47,7 @@
|
|||
"personalInfo": "Personal info",
|
||||
"personalInfoDescription": "Manage your basic information",
|
||||
"privacyPolicy": "Privacy policy",
|
||||
"refreshPage": "Refresh the page",
|
||||
"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",
|
||||
|
@ -56,14 +65,21 @@
|
|||
"shareTheResource": "Share the resource - {{0}}",
|
||||
"shareUser": "Add users to share your resource with",
|
||||
"shareWith": "Share with ",
|
||||
"signedInDevices": "Signed in devices",
|
||||
"signedInDevicesExplanation": "Sign out of any unfamiliar devices.",
|
||||
"signedOutSession": "Signed out {{0}}/{{1}}",
|
||||
"signingIn": "Signing 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.",
|
||||
"somethingWentWrong": "Something went wrong",
|
||||
"somethingWentWrongDescription": "Sorry, an unexpected error has occurred.",
|
||||
"started": "Started",
|
||||
"status": "Status",
|
||||
"termsOfService": "Terms of service",
|
||||
"thirdPartyApp": "Third-party",
|
||||
"tryAgain": "Try again",
|
||||
"unknownOperatingSystem": "Unknown operating system",
|
||||
"unknownUser": "Anonymous",
|
||||
"unShare": "Unshare all",
|
||||
"user": "User",
|
||||
|
|
|
@ -1,7 +1,245 @@
|
|||
import { PageSection } from "@patternfly/react-core";
|
||||
import {
|
||||
Button,
|
||||
DataList,
|
||||
DataListContent,
|
||||
DataListItem,
|
||||
DataListItemRow,
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
Grid,
|
||||
GridItem,
|
||||
Label,
|
||||
Spinner,
|
||||
Split,
|
||||
SplitItem,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
SyncAltIcon,
|
||||
MobileAltIcon,
|
||||
DesktopIcon,
|
||||
} from "@patternfly/react-icons";
|
||||
import { TFuncKey } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { deleteSession, getDevices } from "../api/methods";
|
||||
import {
|
||||
DeviceRepresentation,
|
||||
SessionRepresentation,
|
||||
ClientRepresentation,
|
||||
} from "../api/representations";
|
||||
import { useAlerts } from "../components/alerts/Alerts";
|
||||
import { ContinueCancelModal } from "../components/continue-cancel/ContinueCancelModel";
|
||||
import useFormatter from "../components/formatter/format-date";
|
||||
import { Page } from "../components/page/Page";
|
||||
import { keycloak } from "../keycloak";
|
||||
import { usePromise } from "../utils/usePromise";
|
||||
|
||||
const DeviceActivity = () => (
|
||||
<PageSection>This is the device activity page.</PageSection>
|
||||
);
|
||||
const DeviceActivity = () => {
|
||||
const { t } = useTranslation();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { formatTime } = useFormatter();
|
||||
|
||||
const [devices, setDevices] = useState<DeviceRepresentation[]>();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(key + 1);
|
||||
|
||||
const moveCurrentToTop = (devices: DeviceRepresentation[]) => {
|
||||
let currentDevice = devices[0];
|
||||
|
||||
const index = devices.findIndex((d) => d.current);
|
||||
currentDevice = devices.splice(index, 1)[0];
|
||||
devices.unshift(currentDevice);
|
||||
|
||||
const sessionIndex = currentDevice.sessions.findIndex((s) => s.current);
|
||||
const currentSession = currentDevice.sessions.splice(sessionIndex, 1)[0];
|
||||
currentDevice.sessions.unshift(currentSession);
|
||||
|
||||
setDevices(devices);
|
||||
};
|
||||
|
||||
usePromise((signal) => getDevices({ signal }), moveCurrentToTop, [key]);
|
||||
|
||||
const signOutAll = async () => {
|
||||
await deleteSession();
|
||||
keycloak.logout();
|
||||
};
|
||||
|
||||
const signOutSession = async (
|
||||
session: SessionRepresentation,
|
||||
device: DeviceRepresentation
|
||||
) => {
|
||||
try {
|
||||
await deleteSession(session.id);
|
||||
addAlert(t("signedOutSession", [session.browser, device.os]));
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("errorSignOutMessage", error);
|
||||
}
|
||||
};
|
||||
|
||||
const makeClientsString = (clients: ClientRepresentation[]): string => {
|
||||
let clientsString = "";
|
||||
clients.forEach((client, index) => {
|
||||
let clientName: string;
|
||||
if (client.clientName !== "") {
|
||||
clientName = t(client.clientName as TFuncKey);
|
||||
} else {
|
||||
clientName = client.clientId;
|
||||
}
|
||||
|
||||
clientsString += clientName;
|
||||
|
||||
if (clients.length > index + 1) clientsString += ", ";
|
||||
});
|
||||
|
||||
return clientsString;
|
||||
};
|
||||
|
||||
if (!devices) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
title={t("device-activity")}
|
||||
description={t("signedInDevicesExplanation")}
|
||||
>
|
||||
<Split hasGutter className="pf-u-mb-lg">
|
||||
<SplitItem isFilled>
|
||||
<Title headingLevel="h2" size="xl">
|
||||
{t("signedInDevices")}
|
||||
</Title>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Tooltip content={t("refreshPage")}>
|
||||
<Button
|
||||
aria-describedby="refresh page"
|
||||
id="refresh-page"
|
||||
variant="link"
|
||||
onClick={() => refresh()}
|
||||
icon={<SyncAltIcon />}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{(devices.length > 1 || devices[0].sessions.length > 1) && (
|
||||
<ContinueCancelModal
|
||||
buttonTitle="signOutAllDevices"
|
||||
modalTitle="signOutAllDevices"
|
||||
modalMessage="signOutAllDevicesWarning"
|
||||
onContinue={() => signOutAll()}
|
||||
/>
|
||||
)}
|
||||
</SplitItem>
|
||||
</Split>
|
||||
<DataList
|
||||
className="signed-in-device-list"
|
||||
aria-label={t("signedInDevices")}
|
||||
>
|
||||
<DataListItem aria-labelledby="sessions">
|
||||
{devices.map((device) =>
|
||||
device.sessions.map((session) => (
|
||||
<DataListItemRow key={device.id}>
|
||||
<DataListContent
|
||||
aria-label="device-sessions-content"
|
||||
className="pf-u-flex-grow-1"
|
||||
>
|
||||
<Grid hasGutter>
|
||||
<GridItem span={1} rowSpan={2}>
|
||||
{device.mobile ? <MobileAltIcon /> : <DesktopIcon />}
|
||||
</GridItem>
|
||||
<GridItem sm={8} md={9} span={10}>
|
||||
<span className="pf-u-mr-md session-title">
|
||||
{device.os.toLowerCase().includes("unknown")
|
||||
? t("unknownOperatingSystem")
|
||||
: device.os}{" "}
|
||||
{!device.osVersion.toLowerCase().includes("unknown") &&
|
||||
device.osVersion}{" "}
|
||||
/ {session.browser}
|
||||
</span>
|
||||
{session.current && (
|
||||
<Label color="green">{t("currentSession")}</Label>
|
||||
)}
|
||||
</GridItem>
|
||||
<GridItem
|
||||
className="pf-u-text-align-right"
|
||||
sm={3}
|
||||
md={2}
|
||||
span={1}
|
||||
>
|
||||
{!session.current && (
|
||||
<ContinueCancelModal
|
||||
buttonTitle="doSignOut"
|
||||
modalTitle="doSignOut"
|
||||
buttonVariant="secondary"
|
||||
modalMessage="signOutWarning"
|
||||
onContinue={() => signOutSession(session, device)}
|
||||
/>
|
||||
)}
|
||||
</GridItem>
|
||||
<GridItem span={11}>
|
||||
<DescriptionList
|
||||
className="signed-in-device-grid"
|
||||
columnModifier={{ sm: "2Col", lg: "3Col" }}
|
||||
cols={5}
|
||||
rows={1}
|
||||
>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("ipAddress")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{session.ipAddress}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("lastAccessedOn")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{formatTime(session.lastAccess)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("clients")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{makeClientsString(session.clients)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("started")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{formatTime(session.started)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>
|
||||
{t("expires")}
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{formatTime(session.expires)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</DataListContent>
|
||||
</DataListItemRow>
|
||||
))
|
||||
)}
|
||||
</DataListItem>
|
||||
</DataList>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeviceActivity;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { parseResponse } from "./parse-response";
|
||||
import {
|
||||
ClientRepresentation,
|
||||
DeviceRepresentation,
|
||||
Permission,
|
||||
UserRepresentation,
|
||||
} from "./representations";
|
||||
|
@ -34,6 +35,13 @@ export async function getPermissionRequests(
|
|||
return parseResponse<Permission[]>(response);
|
||||
}
|
||||
|
||||
export async function getDevices({
|
||||
signal,
|
||||
}: CallOptions): Promise<DeviceRepresentation[]> {
|
||||
const response = await request("/sessions/devices", { signal });
|
||||
return parseResponse<DeviceRepresentation[]>(response);
|
||||
}
|
||||
|
||||
export async function getApplications({ signal }: CallOptions = {}): Promise<
|
||||
ClientRepresentation[]
|
||||
> {
|
||||
|
@ -44,3 +52,9 @@ export async function getApplications({ signal }: CallOptions = {}): Promise<
|
|||
export async function deleteConsent(id: string) {
|
||||
return request(`/applications/${id}/consent`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export async function deleteSession(id?: string) {
|
||||
return request(`"/sessions${id ? `/${id}` : ""}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
|
29
apps/account-ui/src/components/formatter/format-date.ts
Normal file
29
apps/account-ui/src/components/formatter/format-date.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
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",
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue