Add more information to session detail page (#1659)

* rename `session` route to `browser-sessions`

* add session detail route

* stubbed route with userid

* get session and display as session tile on session detail page

* improve error message

* useMemo instead of ref

* oauth session detail page

* compat session detail

* link to session detail from compat and oauth sessions
This commit is contained in:
Kerry
2023-09-01 09:59:41 +12:00
committed by GitHub
parent 00a4508d87
commit 08fe0e6441
10 changed files with 349 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useTransition } from "react";
import { Link } from "../Router";
import { FragmentType, graphql, useFragment } from "../gql";
import { Session } from "./Session";
@@ -47,7 +48,7 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ `
}
`);
const endCompatSessionFamily = atomFamily((id: string) => {
export const endCompatSessionFamily = atomFamily((id: string) => {
const endCompatSession = atomWithMutation(END_SESSION_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
@@ -59,7 +60,7 @@ const endCompatSessionFamily = atomFamily((id: string) => {
return endCompatSessionAtom;
});
const simplifyUrl = (url: string): string => {
export const simplifyUrl = (url: string): string => {
let parsed;
try {
parsed = new URL(url);
@@ -93,6 +94,10 @@ const CompatSession: React.FC<{
});
};
const sessionName = (
<Link route={{ type: "session", id: data.deviceId }}>{data.deviceId}</Link>
);
const clientName = data.ssoLogin?.redirectUri
? simplifyUrl(data.ssoLogin.redirectUri)
: undefined;
@@ -100,7 +105,7 @@ const CompatSession: React.FC<{
return (
<Session
id={data.id}
name={data.deviceId}
name={sessionName}
createdAt={data.createdAt}
finishedAt={data.finishedAt || undefined}
clientName={clientName}

View File

@@ -18,6 +18,7 @@ import { atomFamily } from "jotai/utils";
import { atomWithMutation } from "jotai-urql";
import { useTransition } from "react";
import { Link } from "../Router";
import { FragmentType, graphql, useFragment } from "../gql";
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
@@ -39,7 +40,7 @@ export const OAUTH2_SESSION_FRAGMENT = graphql(/* GraphQL */ `
}
`);
type Oauth2SessionType = {
export type Oauth2SessionType = {
id: string;
scope: string;
createdAt: string;
@@ -64,7 +65,7 @@ const END_SESSION_MUTATION = graphql(/* GraphQL */ `
}
`);
const endSessionFamily = atomFamily((id: string) => {
export const endSessionFamily = atomFamily((id: string) => {
const endSession = atomWithMutation(END_SESSION_MUTATION);
// A proxy atom which pre-sets the id variable in the mutation
@@ -96,12 +97,16 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
});
};
const sessionName = getDeviceIdFromScope(data.scope);
const deviceId = getDeviceIdFromScope(data.scope);
const name = deviceId && (
<Link route={{ type: "session", id: deviceId }}>{deviceId}</Link>
);
return (
<Session
id={data.id}
name={sessionName}
name={name}
createdAt={data.createdAt}
finishedAt={data.finishedAt || undefined}
clientName={data.client.clientName}

View File

@@ -13,6 +13,7 @@
// limitations under the License.
import { H6, Body } from "@vector-im/compound-web";
import { ReactNode } from "react";
import Block from "../Block";
import DateTime from "../DateTime";
@@ -25,7 +26,7 @@ const SessionMetadata: React.FC<React.ComponentProps<typeof Body>> = (
export type SessionProps = {
id: string;
name?: string;
name?: string | ReactNode;
createdAt: string;
finishedAt?: string;
clientName?: string;

View File

@@ -0,0 +1,96 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { H3, Button } from "@vector-im/compound-web";
import { useSetAtom } from "jotai";
import { useTransition } from "react";
import { FragmentType, useFragment } from "../../gql";
import BlockList from "../BlockList/BlockList";
import {
COMPAT_SESSION_FRAGMENT,
endCompatSessionFamily,
simplifyUrl,
} from "../CompatSession";
import DateTime from "../DateTime";
import SessionDetails from "./SessionDetails";
type Props = {
session: FragmentType<typeof COMPAT_SESSION_FRAGMENT>;
};
const CompatSessionDetail: React.FC<Props> = ({ session }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(COMPAT_SESSION_FRAGMENT, session);
const endSession = useSetAtom(endCompatSessionFamily(data.id));
// @TODO(kerrya) make this wait for session refresh properly
// https://github.com/matrix-org/matrix-authentication-service/issues/1533
const onSessionEnd = (): void => {
startTransition(() => {
endSession();
});
};
const finishedAt = data.finishedAt
? [{ label: "Finished", value: <DateTime datetime={data.createdAt} /> }]
: [];
const sessionDetails = [
{ label: "ID", value: <code>{data.id}</code> },
{ label: "Device ID", value: <code>{data.deviceId}</code> },
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
...finishedAt,
];
const clientName = data.ssoLogin?.redirectUri
? simplifyUrl(data.ssoLogin.redirectUri)
: undefined;
const clientDetails = [
{ label: "Name", value: clientName },
{
label: "Uri",
value: (
<a target="_blank" href={data.ssoLogin?.redirectUri}>
{data.ssoLogin?.redirectUri}
</a>
),
},
];
return (
<div>
<BlockList>
<H3>{data.deviceId || data.id}</H3>
<SessionDetails title="Session" details={sessionDetails} />
<SessionDetails title="Client" details={clientDetails} />
{!data.finishedAt && (
<Button
kind="destructive"
size="sm"
onClick={onSessionEnd}
disabled={pending}
>
{/* @TODO(kerrya) put this back after pending state works properly */}
{/* { pending && <LoadingSpinner />} */}
End session
</Button>
)}
</BlockList>
</div>
);
};
export default CompatSessionDetail;

View File

@@ -0,0 +1,113 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { H3, Button } from "@vector-im/compound-web";
import { useSetAtom } from "jotai";
import { useTransition } from "react";
import { FragmentType, useFragment } from "../../gql";
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
import BlockList from "../BlockList/BlockList";
import DateTime from "../DateTime";
import {
OAUTH2_SESSION_FRAGMENT,
Oauth2SessionType,
endSessionFamily,
} from "../OAuth2Session";
import SessionDetails from "./SessionDetails";
type Props = {
session: FragmentType<typeof OAUTH2_SESSION_FRAGMENT>;
};
const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
const [pending, startTransition] = useTransition();
const data = useFragment(
OAUTH2_SESSION_FRAGMENT,
session,
) as Oauth2SessionType;
const endSession = useSetAtom(endSessionFamily(data.id));
// @TODO(kerrya) make this wait for session refresh properly
// https://github.com/matrix-org/matrix-authentication-service/issues/1533
const onSessionEnd = (): void => {
startTransition(() => {
endSession();
});
};
const deviceId = getDeviceIdFromScope(data.scope);
const scopes = data.scope.split(" ");
const finishedAt = data.finishedAt
? [{ label: "Finished", value: <DateTime datetime={data.createdAt} /> }]
: [];
const sessionDetails = [
{ label: "ID", value: <code>{data.id}</code> },
{ label: "Device ID", value: <code>{deviceId}</code> },
{ label: "Signed in", value: <DateTime datetime={data.createdAt} /> },
...finishedAt,
{
label: "Scopes",
value: (
<>
{scopes.map((scope) => (
<p>
<code key={scope}>{scope}</code>
</p>
))}
</>
),
},
];
const clientDetails = [
{ label: "Name", value: data.client.clientName },
{ label: "ID", value: <code>{data.client.clientId}</code> },
{
label: "Uri",
value: (
<a target="_blank" href={data.client.clientUri}>
{data.client.clientUri}
</a>
),
},
];
return (
<div>
<BlockList>
<H3>{deviceId || data.id}</H3>
<SessionDetails title="Session" details={sessionDetails} />
<SessionDetails title="Client" details={clientDetails} />
{!data.finishedAt && (
<Button
kind="destructive"
size="sm"
onClick={onSessionEnd}
disabled={pending}
>
{/* @TODO(kerrya) put this back after pending state works properly */}
{/* { pending && <LoadingSpinner />} */}
End session
</Button>
)}
</BlockList>
</div>
);
};
export default OAuth2SessionDetail;

View File

@@ -20,8 +20,9 @@ import { useMemo } from "react";
import { Link } from "../../Router";
import { graphql } from "../../gql/gql";
import CompatSession from "../CompatSession";
import OAuth2Session from "../OAuth2Session";
import CompatSessionDetail from "./CompatSessionDetail";
import OAuth2SessionDetail from "./OAuth2SessionDetail";
const QUERY = graphql(/* GraphQL */ `
query SessionQuery($userId: ID!, $deviceId: String!) {
@@ -70,9 +71,9 @@ const SessionDetail: React.FC<{
const sessionType = session.__typename;
if (sessionType === "Oauth2Session") {
return <OAuth2Session session={session} />;
return <OAuth2SessionDetail session={session} />;
} else {
return <CompatSession session={session} />;
return <CompatSessionDetail session={session} />;
}
};

View File

@@ -0,0 +1,36 @@
/* Copyright 2023 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.list {
display: flex;
flex-direction: column;
margin-top: var(--cpd-space-1x);
gap: var(--cpd-space-1x);
}
.detail-row {
display: flex;
flex-direction: row;
gap: var(--cpd-space-4x);
}
.detail-label {
flex: 0 0 20%;
color: var(--cpd-color-text-secondary);
}
.detail-value {
overflow-wrap: anywhere;
}

View File

@@ -0,0 +1,52 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { H6, Body } from "@vector-im/compound-web";
import { ReactNode } from "react";
import Block from "../Block/Block";
import styles from "./SessionDetails.module.css";
type Detail = { label: string; value: string | ReactNode };
type Props = {
title: string;
details: Detail[];
};
const DetailRow: React.FC<Detail> = ({ label, value }) => (
<li className={styles.detailRow}>
<Body size="sm" weight="semibold" className={styles.detailLabel}>
{label}
</Body>
<Body className={styles.detailValue} size="sm">
{value}
</Body>
</li>
);
const SessionDetails: React.FC<Props> = ({ title, details }) => {
return (
<Block>
<H6>{title}</H6>
<ul className={styles.list}>
{details.map(({ label, value }) => (
<DetailRow key={label} label={label} value={value} />
))}
</ul>
</Block>
);
};
export default SessionDetails;

View File

@@ -8,7 +8,13 @@ exports[`<CompatSession /> > renders a finished session 1`] = `
className="_font-body-md-semibold_1g2sj_69 _sessionName_634806"
title="session-id"
>
abcd1234
<a
className=""
href="/session/abcd1234"
onClick={[Function]}
>
abcd1234
</a>
</h6>
<p
className="_font-body-sm-semibold_1g2sj_49 _sessionMetadata_634806"
@@ -49,7 +55,13 @@ exports[`<CompatSession /> > renders an active session 1`] = `
className="_font-body-md-semibold_1g2sj_69 _sessionName_634806"
title="session-id"
>
abcd1234
<a
className=""
href="/session/abcd1234"
onClick={[Function]}
>
abcd1234
</a>
</h6>
<p
className="_font-body-sm-semibold_1g2sj_49 _sessionMetadata_634806"

View File

@@ -8,7 +8,13 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
className="_font-body-md-semibold_1g2sj_69 _sessionName_634806"
title="session-id"
>
abcd1234
<a
className=""
href="/session/abcd1234"
onClick={[Function]}
>
abcd1234
</a>
</h6>
<p
className="_font-body-sm-semibold_1g2sj_49 _sessionMetadata_634806"
@@ -49,7 +55,13 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
className="_font-body-md-semibold_1g2sj_69 _sessionName_634806"
title="session-id"
>
abcd1234
<a
className=""
href="/session/abcd1234"
onClick={[Function]}
>
abcd1234
</a>
</h6>
<p
className="_font-body-sm-semibold_1g2sj_49 _sessionMetadata_634806"