1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-23 11:02:35 +03:00

Make the session list way better

This commit is contained in:
Quentin Gliech
2024-02-27 12:29:26 +01:00
parent f5e47edbb9
commit bfc088bdd0
17 changed files with 995 additions and 832 deletions

View File

@@ -119,21 +119,19 @@
"title": "Crypto identity reset temporarily allowed"
}
},
"selectable_session": {
"label": "Select session"
},
"session": {
"current_badge": "Current",
"current": "Current",
"device_id_label": "Device ID",
"finished_date": "Finished <datetime/>",
"finished_label": "Finished",
"id_label": "ID",
"ip_label": "IP Address",
"last_active_label": "Last Active",
"last_auth_label": "Last Authentication",
"name_for_platform": "{{name}} for {{platform}}",
"scopes_label": "Scopes",
"signed_in_date": "Signed in <datetime/>",
"signed_in_label": "Signed in",
"unknown_browser": "Unknown browser",
"unknown_device": "Unknown device",
"uri_label": "Uri",
"user_id_label": "User ID",
"username_label": "User name"

View File

@@ -12,14 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Badge } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useMutation } from "urql";
import { FragmentType, graphql, useFragment } from "../gql";
import { DeviceType } from "../gql/graphql";
import DateTime from "./DateTime";
import EndSessionButton from "./Session/EndSessionButton";
import Session from "./Session/Session";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
const FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_session on BrowserSession {
@@ -77,38 +82,84 @@ type Props = {
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
const data = useFragment(FRAGMENT, session);
const { t } = useTranslation();
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
const deviceType = data.userAgent?.deviceType ?? DeviceType.Unknown;
let deviceName: string | null = null;
let clientName: string | null = null;
// If we have a model, use that as the device name, and the browser (+ OS) as the client name
if (data.userAgent?.model) {
deviceName = data.userAgent.model;
if (data.userAgent?.name) {
if (data.userAgent?.os) {
clientName = t("frontend.session.name_for_platform", {
name: data.userAgent.name,
platform: data.userAgent.os,
});
} else {
clientName = data.userAgent.name;
}
}
} else {
// Else use the browser as the device name
deviceName = data.userAgent?.name ?? t("frontend.session.unknown_browser");
// and if we have an OS, use that as the client name
clientName = data.userAgent?.os ?? null;
}
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
let sessionName = "Browser session";
if (data.userAgent) {
if (data.userAgent.model && data.userAgent.name) {
sessionName = `${data.userAgent.name} on ${data.userAgent.model}`;
} else if (data.userAgent.name && data.userAgent.os) {
sessionName = `${data.userAgent.name} on ${data.userAgent.os}`;
} else if (data.userAgent.name) {
sessionName = data.userAgent.name;
}
}
return (
<Session
id={data.id}
name={sessionName}
createdAt={createdAt}
finishedAt={finishedAt}
isCurrent={isCurrent}
deviceType={data.userAgent?.deviceType}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
<Card.Root>
<Card.LinkBody
to="/sessions/$id"
params={{ id: data.id }}
disabled={!!data.finishedAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
{clientName && <Card.Client name={clientName} />}
</Card.Header>
<Card.Metadata>
{lastActiveAt && !isCurrent && (
<Card.Info label={t("frontend.session.last_active_label")}>
<LastActive lastActive={lastActiveAt} />
</Card.Info>
)}
<Card.Info label={t("frontend.session.signed_in_label")}>
<DateTime datetime={createdAt} />
</Card.Info>
{isCurrent && (
<Badge kind="success" className="self-center">
{t("frontend.session.current")}
</Badge>
)}
</Card.Metadata>
</Card.LinkBody>
{!data.finishedAt && (
<Card.Action>
<EndSessionButton endSession={onSessionEnd}>
<Card.Body compact>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
{clientName && <Card.Client name={clientName} />}
</Card.Header>
</Card.Body>
</EndSessionButton>
</Card.Action>
)}
</Card.Root>
);
};

View File

@@ -13,12 +13,16 @@
// limitations under the License.
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { useMutation } from "urql";
import { FragmentType, graphql, useFragment } from "../gql";
import { DeviceType } from "../gql/graphql";
import { Session } from "./Session";
import DateTime from "./DateTime";
import EndSessionButton from "./Session/EndSessionButton";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_session on CompatSession {
@@ -29,7 +33,6 @@ export const FRAGMENT = graphql(/* GraphQL */ `
lastActiveIp
lastActiveAt
userAgent {
raw
name
os
model
@@ -78,6 +81,7 @@ export const simplifyUrl = (url: string): string => {
const CompatSession: React.FC<{
session: FragmentType<typeof FRAGMENT>;
}> = ({ session }) => {
const { t } = useTranslation();
const data = useFragment(FRAGMENT, session);
const [, endCompatSession] = useMutation(END_SESSION_MUTATION);
@@ -85,39 +89,68 @@ const CompatSession: React.FC<{
await endCompatSession({ id: data.id });
};
let clientName = data.ssoLogin?.redirectUri
const clientName = data.ssoLogin?.redirectUri
? simplifyUrl(data.ssoLogin.redirectUri)
: undefined;
if (data.userAgent) {
if (data.userAgent.model && data.userAgent.name) {
clientName = `${data.userAgent.name} on ${data.userAgent.model}`;
} else if (data.userAgent.name && data.userAgent.os) {
clientName = `${data.userAgent.name} on ${data.userAgent.os}`;
} else if (data.userAgent.name) {
clientName = data.userAgent.name;
}
}
const deviceType = data.userAgent?.deviceType ?? DeviceType.Unknown;
const deviceName =
data.userAgent?.model ??
(data.userAgent?.name
? data.userAgent?.os
? t("frontend.session.name_for_platform", {
name: data.userAgent.name,
platform: data.userAgent.os,
})
: data.userAgent.name
: t("frontend.session.unknown_device"));
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
return (
<Session
id={data.id}
name={data.deviceId}
createdAt={createdAt}
finishedAt={finishedAt}
clientName={clientName}
deviceType={data.userAgent?.deviceType}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
<Card.Root>
<Card.LinkBody
to="/sessions/$id"
params={{ id: data.id }}
disabled={!!data.finishedAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
{clientName && <Card.Client name={clientName} />}
</Card.Header>
<Card.Metadata>
{lastActiveAt && (
<Card.Info label={t("frontend.session.last_active_label")}>
<LastActive lastActive={lastActiveAt} />
</Card.Info>
)}
<Card.Info label={t("frontend.session.signed_in_label")}>
<DateTime datetime={createdAt} />
</Card.Info>
<Card.Info label={t("frontend.session.device_id_label")}>
{data.deviceId}
</Card.Info>
</Card.Metadata>
</Card.LinkBody>
{!data.finishedAt && (
<Card.Action>
<EndSessionButton endSession={onSessionEnd}>
<Card.Body compact>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
{clientName && <Card.Client name={clientName} />}
</Card.Header>
</Card.Body>
</EndSessionButton>
</Card.Action>
)}
</Card.Root>
);
};

View File

@@ -13,14 +13,17 @@
// limitations under the License.
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { useMutation } from "urql";
import { FragmentType, graphql, useFragment } from "../gql";
import { DeviceType, Oauth2ApplicationType } from "../gql/graphql";
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
import { Session } from "./Session";
import DateTime from "./DateTime";
import EndSessionButton from "./Session/EndSessionButton";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Session_session on Oauth2Session {
@@ -32,9 +35,9 @@ export const FRAGMENT = graphql(/* GraphQL */ `
lastActiveAt
userAgent {
name
model
os
osVersion
deviceType
}
@@ -77,6 +80,7 @@ type Props = {
};
const OAuth2Session: React.FC<Props> = ({ session }) => {
const { t } = useTranslation();
const data = useFragment(FRAGMENT, session);
const [, endSession] = useMutation(END_SESSION_MUTATION);
@@ -87,7 +91,6 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
const deviceId = getDeviceIdFromScope(data.scope);
const createdAt = parseISO(data.createdAt);
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
const lastActiveAt = data.lastActiveAt
? parseISO(data.lastActiveAt)
: undefined;
@@ -98,26 +101,67 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
: data.userAgent?.deviceType) ??
getDeviceTypeFromClientAppType(data.client.applicationType);
let clientName = data.client.clientName || data.client.clientId || undefined;
const clientName = data.client.clientName || data.client.clientId;
if (data.userAgent?.model) {
clientName = `${clientName} on ${data.userAgent.model}`;
}
const deviceName =
data.userAgent?.model ??
(data.userAgent?.name
? data.userAgent?.os
? t("frontend.session.name_for_platform", {
name: data.userAgent.name,
platform: data.userAgent.os,
})
: data.userAgent.name
: t("frontend.session.unknown_device"));
return (
<Session
id={data.id}
name={deviceId}
createdAt={createdAt}
finishedAt={finishedAt}
clientName={clientName}
clientLogoUri={data.client.logoUri || undefined}
deviceType={deviceType}
lastActiveIp={data.lastActiveIp || undefined}
lastActiveAt={lastActiveAt}
<Card.Root>
<Card.LinkBody
to="/sessions/$id"
params={{ id: data.id }}
disabled={!!data.finishedAt}
>
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</Session>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
<Card.Client
name={clientName}
logoUri={data.client.logoUri ?? undefined}
/>
</Card.Header>
<Card.Metadata>
{lastActiveAt && (
<Card.Info label={t("frontend.session.last_active_label")}>
<LastActive lastActive={lastActiveAt} />
</Card.Info>
)}
<Card.Info label={t("frontend.session.signed_in_label")}>
<DateTime datetime={createdAt} />
</Card.Info>
{deviceId && (
<Card.Info label={t("frontend.session.device_id_label")}>
{deviceId}
</Card.Info>
)}
</Card.Metadata>
</Card.LinkBody>
{!data.finishedAt && (
<Card.Action>
<EndSessionButton endSession={onSessionEnd}>
<Card.Body compact>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
<Card.Client
name={clientName}
logoUri={data.client.logoUri ?? undefined}
/>
</Card.Header>
</Card.Body>
</EndSessionButton>
</Card.Action>
)}
</Card.Root>
);
};

View File

@@ -14,12 +14,11 @@
*/
.icon {
color: var(--cpd-color-icon-primary);
background-color: var(--cpd-color-bg-subtle-primary);
flex: 0 0 var(--cpd-space-4x);
color: var(--cpd-color-icon-secondary);
background-color: var(--cpd-color-bg-subtle-secondary);
box-sizing: content-box;
height: var(--cpd-space-4x);
width: var(--cpd-space-4x);
height: var(--cpd-space-6x);
width: var(--cpd-space-6x);
padding: var(--cpd-space-2x);
border-radius: var(--cpd-space-2x);
}

View File

@@ -1,55 +0,0 @@
/* 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.
*/
.session {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap:var(--cpd-space-4x);
}
.container {
display: flex;
flex: 1 1 100%;
flex-direction: column;
align-items: flex-start;
}
.session-name {
overflow: hidden;
text-overflow: ellipsis;
margin-top: var(--cpd-space-1x);
margin-bottom: var(--cpd-space-1x);
}
.session-metadata {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--cpd-space-1x);
color: var(--cpd-color-text-secondary);
&[data-finished] {
color: var(--cpd-color-text-critical-primary);
}
}
.session-actions {
margin-top: var(--cpd-space-1x);
display: flex;
flex-direction: row;
justify-content: flex-end;
width: 100%;
}

View File

@@ -1,99 +0,0 @@
// 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.
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { ReactElement } from "react";
import BlockList from "../BlockList/BlockList";
import Session from "./Session";
const meta = {
title: "UI/Session/Session",
component: Session,
tags: ["autodocs"],
argTypes: {
createdAt: { control: { type: "date" } },
finishedAt: { control: { type: "date" } },
lastActiveAt: { control: { type: "date" } },
},
decorators: [
(Story): ReactElement => (
<div style={{ width: "378px" }}>
<BlockList>
<Story />
</BlockList>
</div>
),
],
} satisfies Meta<typeof Session>;
export default meta;
type Story = StoryObj<typeof Session>;
const defaultProps = {
id: "oauth2_session:01H5VAGA5NYTKJVXP3HMMKDJQ0",
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
};
export const BasicSession: Story = {
args: {
...defaultProps,
name: "KlTqK9CRt3",
lastActiveIp: "2001:8003:c4614:f501:3091:888a:49c7",
lastActiveAt: parseISO("2023-07-29T03:35:17.451292+00:00"),
clientName: "Element",
},
};
export const BasicFinishedSession: Story = {
args: {
...defaultProps,
name: "Chrome on Android",
finishedAt: parseISO("2023-06-30T03:35:17.451292+00:00"),
},
};
export const WithClientLogo: Story = {
args: {
...defaultProps,
name: "KlTqK9CRt3",
clientName: "Element",
clientLogoUri: "https://element.io/images/logo-mark-primary.svg",
},
};
export const WithMinimumProps: Story = {
args: defaultProps,
};
export const WithChildActions: Story = {
args: {
...defaultProps,
name: "KlTqK9CRt3",
clientName: "Element",
clientLogoUri: "https://element.io/images/logo-mark-primary.svg",
children: (
<>
<Button size="sm" onClick={(): void => {}} kind="destructive">
End session
</Button>
</>
),
},
};

View File

@@ -1,93 +0,0 @@
// 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.
// @vitest-environment happy-dom
import { parseISO } from "date-fns";
import { create } from "react-test-renderer";
import { describe, expect, it, beforeAll } from "vitest";
import { mockLocale } from "../../test-utils/mockLocale";
import { DummyRouter } from "../../test-utils/router";
import Session from "./Session";
describe("<Session />", () => {
const defaultProps = {
id: "session-id",
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
};
const finishedAt = parseISO("2023-06-29T03:35:19.451292+00:00");
beforeAll(() => mockLocale());
it("renders an active session", () => {
const component = create(
<DummyRouter>
<Session {...defaultProps} />
</DummyRouter>,
);
expect(component.toJSON()).toMatchSnapshot();
});
it("renders a finished session", () => {
const component = create(
<DummyRouter>
<Session {...defaultProps} finishedAt={finishedAt} />
</DummyRouter>,
);
expect(component.toJSON()).toMatchSnapshot();
});
it("uses session name when truthy", () => {
const name = "test session name";
const component = create(
<DummyRouter>
<Session {...defaultProps} finishedAt={finishedAt} name={name} />
</DummyRouter>,
);
expect(component.toJSON()).toMatchSnapshot();
});
it("uses client name when truthy", () => {
const clientName = "Element";
const component = create(
<DummyRouter>
<Session
{...defaultProps}
finishedAt={finishedAt}
clientName={clientName}
clientLogoUri="https://client.org/logo.png"
/>
</DummyRouter>,
);
expect(component.toJSON()).toMatchSnapshot();
});
it("renders ip address", () => {
const clientName = "Element";
const component = create(
<DummyRouter>
<Session
{...defaultProps}
finishedAt={finishedAt}
clientName={clientName}
lastActiveIp="127.0.0.1"
/>
</DummyRouter>,
);
expect(component.toJSON()).toMatchSnapshot();
});
});

View File

@@ -1,110 +0,0 @@
// 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.
import { Link } from "@tanstack/react-router";
import { H6, Text, Badge } from "@vector-im/compound-web";
import { Trans, useTranslation } from "react-i18next";
import { DeviceType } from "../../gql/graphql";
import Block from "../Block";
import DateTime from "../DateTime";
import ClientAvatar from "./ClientAvatar";
import DeviceTypeIcon from "./DeviceTypeIcon";
import LastActive from "./LastActive";
import styles from "./Session.module.css";
const SessionMetadata: React.FC<React.ComponentProps<typeof Text>> = (
props,
) => <Text {...props} size="sm" className={styles.sessionMetadata} />;
type SessionProps = {
id: string;
name?: string;
createdAt: Date;
finishedAt?: Date;
clientName?: string;
clientLogoUri?: string;
isCurrent?: boolean;
deviceType?: DeviceType;
lastActiveIp?: string;
lastActiveAt?: Date;
};
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
id,
name,
createdAt,
finishedAt,
clientName,
clientLogoUri,
lastActiveIp,
lastActiveAt,
isCurrent,
children,
deviceType,
}) => {
const { t } = useTranslation();
return (
<Block className={styles.session}>
<DeviceTypeIcon deviceType={deviceType || DeviceType.Unknown} />
<div className={styles.container}>
{isCurrent && (
<Badge kind="success">{t("frontend.session.current_badge")}</Badge>
)}
<H6 className={styles.sessionName} title={id}>
<Link to="/sessions/$id" params={{ id }}>
{name || id}
</Link>
</H6>
<SessionMetadata weight="semibold">
<Trans
i18nKey="frontend.session.signed_in_date"
components={{ datetime: <DateTime datetime={createdAt} /> }}
/>
</SessionMetadata>
{!!finishedAt && (
<SessionMetadata weight="semibold" data-finished={true}>
<Trans
i18nKey="frontend.session.finished_date"
components={{ datetime: <DateTime datetime={finishedAt} /> }}
/>
</SessionMetadata>
)}
{!!lastActiveAt && (
<SessionMetadata>
<LastActive lastActive={lastActiveAt} />
</SessionMetadata>
)}
{!!lastActiveIp && <SessionMetadata>{lastActiveIp}</SessionMetadata>}
{!!clientName && (
<SessionMetadata>
<ClientAvatar
size="var(--cpd-space-4x)"
name={clientName}
logoUri={clientLogoUri}
/>{" "}
<SessionMetadata weight="semibold" as="span">
{clientName}
</SessionMetadata>
</SessionMetadata>
)}
{!!children && <div className={styles.sessionActions}>{children}</div>}
</div>
</Block>
);
};
export default Session;

View File

@@ -0,0 +1,165 @@
/* Copyright 2024 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.
*/
.root {
position: relative;
}
@media screen and (min-width: 768px) {
.root:has(.action) .body .header {
/* We can't exactly know the width of the action button,
* so we use a somewhat arbitrary safe value:
* - the button padding is 4x + 5x
* - the icon is 5x wide
* - a 2x safe margin
* - the approximate width of the button text "Sign out"
*
* Of course that depends on the translation, but it's a good start
*/
padding-inline-end: calc(var(--cpd-space-16x) + 10ch);
}
}
@media screen and (max-width: 767px) {
.root:has(.action) .body {
/* On small screen, the action button is at the bottom, and we can accurately
* calculate the height of the button:
*
* - the button padding is 1x + 1x
* - its line height is 6.5x
* - plus 2x 1px of border (so 0.5x)
* - the 4x margin on top of the button
* - the regular 6x padding
*/
padding-block-end: calc(var(--cpd-space-9x) + var(--cpd-space-6x) + var(--cpd-space-6x));
}
}
.body {
display: flex;
gap: var(--cpd-space-4x);
flex-direction: column;
border-radius: var(--cpd-space-4x);
background-color: var(--cpd-color-bg-canvas-default);
outline: 1px solid var(--cpd-color-border-interactive-secondary);
outline-offset: -1px;
box-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
padding: var(--cpd-space-6x);
&.disabled {
outline-color: var(--cpd-color-border-disabled);
background-color: var(--cpd-color-bg-canvas-disabled);
box-shadow: none;
}
&.compact {
box-shadow: none;
padding: var(--cpd-space-3x);
}
}
a.body:not(.disabled) {
transition-property: outline-color, box-shadow;
transition-duration: 0.1s;
transition-timing-function: linear;
&:hover,
&:focus-visible {
box-shadow: none;
outline: 2px solid var(--cpd-color-border-interactive-hovered);
outline-offset: -2px;
}
&:focus-visible {
outline-color: var(--cpd-color-border-focused);
}
}
@media screen and (min-width: 768px) {
.action {
position: absolute;
/* This padding creates a safe area for the action button */
padding: var(--cpd-space-6x) var(--cpd-space-6x) var(--cpd-space-2x) var(--cpd-space-2x);
inset-block-start: 0;
inset-inline-end: 0;
}
}
@media screen and (max-width: 767px) {
.action {
display: flex;
flex-direction: column;
position: absolute;
padding: var(--cpd-space-6x);
inset-block-end: 0;
inset-inline: 0;
}
}
.header {
display: flex;
gap: var(--cpd-space-4x);
align-items: center;
& .content {
display: flex;
flex-direction: column;
/* This makes sure it can shrink, and that the text doesn't overflow */
flex: 0 1 auto;
min-width: 0;
& .name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font: var(--cpd-font-body-md-semibold);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
}
& .client {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font: var(--cpd-font-body-sm-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
color: var(--cpd-color-text-secondary);
& img {
margin-inline-end: var(--cpd-space-1x);
}
}
}
}
.metadata {
display: flex;
flex-wrap: wrap;
gap: var(--cpd-space-4x) var(--cpd-space-10x);
& .key {
font: var(--cpd-font-body-sm-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
color: var(--cpd-color-text-secondary);
}
& .value {
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
}
}

View File

@@ -0,0 +1,75 @@
// Copyright 2024 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 type { Meta, StoryObj } from "@storybook/react";
import IconSignOut from "@vector-im/compound-design-tokens/icons/sign-out.svg?react";
import { Button } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { DeviceType } from "../../gql/graphql";
import * as Card from "./SessionCard";
const Template: React.FC<{
deviceType: DeviceType;
deviceName: string;
clientName?: string;
disabled?: boolean;
}> = ({ deviceType, deviceName, clientName, disabled }) => {
const { t } = useTranslation();
return (
<Card.Root>
<Card.Body disabled={disabled}>
<Card.Header type={deviceType}>
<Card.Name name={deviceName} />
{clientName && <Card.Client name={clientName} />}
</Card.Header>
<Card.Metadata>
<Card.Info label="Last active">2 hours ago</Card.Info>
<Card.Info label="Signed in">NOV 30, 2023</Card.Info>
<Card.Info label="Device ID">XXXXXX</Card.Info>
</Card.Metadata>
</Card.Body>
{!disabled && (
<Card.Action>
<Button kind="secondary" destructive size="sm" Icon={IconSignOut}>
{t("frontend.end_session_button.text")}
</Button>
</Card.Action>
)}
</Card.Root>
);
};
const meta = {
title: "UI/Session/Card",
component: Template,
args: {
disabled: false,
deviceName: "MacBook Pro 16",
clientName: "Firefox",
deviceType: DeviceType.Pc,
},
argTypes: {
deviceType: { control: "select", options: Object.values(DeviceType) },
disabled: { control: "boolean" },
deviceName: { control: "text" },
clientName: { control: "text" },
},
} satisfies Meta<typeof Template>;
export default meta;
type Story = StoryObj<typeof Template>;
export const Basic: Story = {};

View File

@@ -0,0 +1,101 @@
// Copyright 2024 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 { LinkComponent, useLinkProps } from "@tanstack/react-router";
import cx from "classnames";
import { forwardRef } from "react";
import { DeviceType } from "../../gql/graphql";
import ClientAvatar from "../Session/ClientAvatar";
import DeviceTypeIcon from "../Session/DeviceTypeIcon";
import styles from "./SessionCard.module.css";
export const Root: React.FC<React.PropsWithChildren> = ({ children }) => (
<section className={styles.root}>{children}</section>
);
type BodyProps = React.PropsWithChildren<{
disabled?: boolean;
compact?: boolean;
}>;
export const LinkBody: LinkComponent = forwardRef<
HTMLAnchorElement,
Parameters<typeof useLinkProps>[0] & BodyProps
>(({ children, disabled, compact, ...props }, ref) => {
const linkProps = useLinkProps({
className: cx(
styles.body,
compact && styles.compact,
disabled && styles.disabled,
),
...props,
});
return (
<a ref={ref} {...linkProps}>
{children}
</a>
);
}) as LinkComponent;
export const Body: React.FC<BodyProps> = ({ children, compact, disabled }) => (
<div
className={cx(
styles.body,
compact && styles.compact,
disabled && styles.disabled,
)}
>
{children}
</div>
);
type HeaderProps = React.PropsWithChildren<{ type: DeviceType }>;
export const Header: React.FC<HeaderProps> = ({ type, children }) => (
<header className={styles.header}>
<DeviceTypeIcon deviceType={type} />
<div className={styles.content}>{children}</div>
</header>
);
type NameProps = { name: string };
export const Name: React.FC<NameProps> = ({ name }) => (
<div className={styles.name}>{name}</div>
);
type ClientProps = { name: string; logoUri?: string };
export const Client: React.FC<ClientProps> = ({ name, logoUri }) => (
<div className={styles.client}>
<ClientAvatar name={name} size="var(--cpd-space-5x)" logoUri={logoUri} />
{name}
</div>
);
export const Metadata: React.FC<React.PropsWithChildren> = ({ children }) => (
<ul className={styles.metadata}>{children}</ul>
);
export const Info: React.FC<React.PropsWithChildren<{ label: string }>> = ({
label,
children,
}) => (
<li>
<div className={styles.key}>{label}</div>
<div className={styles.value}>{children}</div>
</li>
);
export const Action: React.FC<React.PropsWithChildren> = ({ children }) => (
<div className={styles.action}>{children}</div>
);

View File

@@ -1,4 +1,4 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
// Copyright 2024 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.
@@ -12,4 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export { default as Session } from "./Session";
export {
Root,
LinkBody,
Body,
Header,
Name,
Client,
Metadata,
Info,
Action,
} from "./SessionCard";

View File

@@ -1,9 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<CompatSession /> > renders a finished session 1`] = `
<div
className="_block_17898c _session_634806"
<section
className="_root_e2909e"
>
<a
className="_body_e2909e _disabled_e2909e"
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
>
<header
className="_header_e2909e"
>
<svg
aria-label="Unknown device type"
className="_icon_e677aa"
@@ -21,68 +34,73 @@ exports[`<CompatSession /> > renders a finished session 1`] = `
/>
</svg>
<div
className="_container_634806"
className="_content_e2909e"
>
<h6
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
title="session-id"
<div
className="_name_e2909e"
>
<a
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
Unknown device
</div>
<div
className="_client_e2909e"
>
abcd1234
</a>
</h6>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
element.io
</div>
</div>
</header>
<ul
className="_metadata_e2909e"
>
<li>
<div
className="_key_e2909e"
>
Signed in
</div>
<div
className="_value_e2909e"
>
<time
dateTime="2023-06-29T03:35:17Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
data-finished={true}
>
Finished
<time
dateTime="2023-06-29T03:35:19Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
<span
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
>
element.io
</span>
</p>
</div>
</div>
</li>
<li>
<div
className="_key_e2909e"
>
Device ID
</div>
<div
className="_value_e2909e"
>
abcd1234
</div>
</li>
</ul>
</a>
</section>
`;
exports[`<CompatSession /> > renders an active session 1`] = `
<div
className="_block_17898c _session_634806"
<section
className="_root_e2909e"
>
<a
className="_body_e2909e"
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
>
<header
className="_header_e2909e"
>
<svg
aria-label="Unknown device type"
className="_icon_e677aa"
@@ -100,51 +118,55 @@ exports[`<CompatSession /> > renders an active session 1`] = `
/>
</svg>
<div
className="_container_634806"
className="_content_e2909e"
>
<h6
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
title="session-id"
<div
className="_name_e2909e"
>
<a
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
Unknown device
</div>
<div
className="_client_e2909e"
>
abcd1234
</a>
</h6>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
element.io
</div>
</div>
</header>
<ul
className="_metadata_e2909e"
>
<li>
<div
className="_key_e2909e"
>
Signed in
</div>
<div
className="_value_e2909e"
>
<time
dateTime="2023-06-29T03:35:17Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
<span
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
>
element.io
</span>
</p>
</div>
</li>
<li>
<div
className="_sessionActions_634806"
className="_key_e2909e"
>
Device ID
</div>
<div
className="_value_e2909e"
>
abcd1234
</div>
</li>
</ul>
</a>
<div
className="_action_e2909e"
>
<button
aria-controls="radix-:r0:"
@@ -174,6 +196,5 @@ exports[`<CompatSession /> > renders an active session 1`] = `
Sign out
</button>
</div>
</div>
</div>
</section>
`;

View File

@@ -1,9 +1,22 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<OAuth2Session /> > renders a finished session 1`] = `
<div
className="_block_17898c _session_634806"
<section
className="_root_e2909e"
>
<a
className="_body_e2909e _disabled_e2909e"
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
>
<header
className="_header_e2909e"
>
<svg
aria-label="Computer"
className="_icon_e677aa"
@@ -18,68 +31,73 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
/>
</svg>
<div
className="_container_634806"
className="_content_e2909e"
>
<h6
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
title="session-id"
<div
className="_name_e2909e"
>
<a
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
Unknown device
</div>
<div
className="_client_e2909e"
>
abcd1234
</a>
</h6>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
Element
</div>
</div>
</header>
<ul
className="_metadata_e2909e"
>
<li>
<div
className="_key_e2909e"
>
Signed in
</div>
<div
className="_value_e2909e"
>
<time
dateTime="2023-06-29T03:35:17Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
data-finished={true}
>
Finished
<time
dateTime="2023-06-29T03:35:19Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
<span
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
>
Element
</span>
</p>
</div>
</div>
</li>
<li>
<div
className="_key_e2909e"
>
Device ID
</div>
<div
className="_value_e2909e"
>
abcd1234
</div>
</li>
</ul>
</a>
</section>
`;
exports[`<OAuth2Session /> > renders an active session 1`] = `
<div
className="_block_17898c _session_634806"
<section
className="_root_e2909e"
>
<a
className="_body_e2909e"
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
>
<header
className="_header_e2909e"
>
<svg
aria-label="Computer"
className="_icon_e677aa"
@@ -94,51 +112,55 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
/>
</svg>
<div
className="_container_634806"
className="_content_e2909e"
>
<h6
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
title="session-id"
<div
className="_name_e2909e"
>
<a
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
Unknown device
</div>
<div
className="_client_e2909e"
>
abcd1234
</a>
</h6>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
Element
</div>
</div>
</header>
<ul
className="_metadata_e2909e"
>
<li>
<div
className="_key_e2909e"
>
Signed in
</div>
<div
className="_value_e2909e"
>
<time
dateTime="2023-06-29T03:35:17Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
<span
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
>
Element
</span>
</p>
</div>
</li>
<li>
<div
className="_sessionActions_634806"
className="_key_e2909e"
>
Device ID
</div>
<div
className="_value_e2909e"
>
abcd1234
</div>
</li>
</ul>
</a>
<div
className="_action_e2909e"
>
<button
aria-controls="radix-:r0:"
@@ -168,14 +190,26 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
Sign out
</button>
</div>
</div>
</div>
</section>
`;
exports[`<OAuth2Session /> > renders correct icon for a native session 1`] = `
<div
className="_block_17898c _session_634806"
<section
className="_root_e2909e"
>
<a
className="_body_e2909e _disabled_e2909e"
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
>
<header
className="_header_e2909e"
>
<svg
aria-label="Mobile"
className="_icon_e677aa"
@@ -190,60 +224,52 @@ exports[`<OAuth2Session /> > renders correct icon for a native session 1`] = `
/>
</svg>
<div
className="_container_634806"
className="_content_e2909e"
>
<h6
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
title="session-id"
<div
className="_name_e2909e"
>
<a
href="/sessions/session-id"
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
onTouchStart={[Function]}
style={{}}
Unknown device
</div>
<div
className="_client_e2909e"
>
abcd1234
</a>
</h6>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
Element
</div>
</div>
</header>
<ul
className="_metadata_e2909e"
>
<li>
<div
className="_key_e2909e"
>
Signed in
</div>
<div
className="_value_e2909e"
>
<time
dateTime="2023-06-29T03:35:17Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
data-finished={true}
>
Finished
<time
dateTime="2023-06-29T03:35:19Z"
>
Thu, 29 Jun 2023, 03:35
</time>
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
1.2.3.4
</p>
<p
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
>
<span
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
>
Element
</span>
</p>
</div>
</div>
</li>
<li>
<div
className="_key_e2909e"
>
Device ID
</div>
<div
className="_value_e2909e"
>
abcd1234
</div>
</li>
</ul>
</a>
</section>
`;

View File

@@ -19,11 +19,11 @@ const documents = {
types.EndBrowserSessionDocument,
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
types.OAuth2Client_DetailFragmentDoc,
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
types.CompatSession_SessionFragmentDoc,
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
types.EndCompatSessionDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
types.OAuth2Session_SessionFragmentDoc,
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
types.EndOAuth2SessionDocument,
@@ -119,8 +119,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -131,8 +131,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"];
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1237,7 +1237,6 @@ export type CompatSession_SessionFragment = {
lastActiveAt?: string | null;
userAgent?: {
__typename?: "UserAgent";
raw: string;
name?: string | null;
os?: string | null;
model?: string | null;
@@ -1277,9 +1276,9 @@ export type OAuth2Session_SessionFragment = {
lastActiveAt?: string | null;
userAgent?: {
__typename?: "UserAgent";
name?: string | null;
model?: string | null;
os?: string | null;
osVersion?: string | null;
deviceType: DeviceType;
} | null;
client: {
@@ -1897,7 +1896,6 @@ export const CompatSession_SessionFragmentDoc = {
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "raw" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "os" } },
{ kind: "Field", name: { kind: "Name", value: "model" } },
@@ -1946,9 +1944,9 @@ export const OAuth2Session_SessionFragmentDoc = {
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "model" } },
{ kind: "Field", name: { kind: "Name", value: "os" } },
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
],
},
@@ -2573,9 +2571,9 @@ export const EndOAuth2SessionDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "model" } },
{ kind: "Field", name: { kind: "Name", value: "os" } },
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
],
},
@@ -4233,7 +4231,6 @@ export const AppSessionsListQueryDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "raw" } },
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "os" } },
{ kind: "Field", name: { kind: "Name", value: "model" } },
@@ -4277,9 +4274,9 @@ export const AppSessionsListQueryDocument = {
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "name" } },
{ kind: "Field", name: { kind: "Name", value: "model" } },
{ kind: "Field", name: { kind: "Name", value: "os" } },
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
],
},