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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
113
frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx
Normal file
113
frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx
Normal 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;
|
||||
@@ -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} />;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
52
frontend/src/components/SessionDetail/SessionDetails.tsx
Normal file
52
frontend/src/components/SessionDetail/SessionDetails.tsx
Normal 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;
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user