From 8da8c9557386cfeb7f17542b490f30b261363fb6 Mon Sep 17 00:00:00 2001
From: Quentin Gliech
Date: Thu, 13 Feb 2025 15:48:19 +0100
Subject: [PATCH] Polish the session details
---
frontend/locales/en.json | 7 +-
.../src/components/Block/Block.module.css | 25 -
.../src/components/Block/Block.stories.tsx | 41 --
frontend/src/components/Block/Block.test.tsx | 39 -
frontend/src/components/Block/Block.tsx | 33 -
.../Block/__snapshots__/Block.test.tsx.snap | 41 --
frontend/src/components/Block/index.ts | 7 -
.../components/BlockList/BlockList.module.css | 13 -
.../BlockList/BlockList.stories.tsx | 36 -
.../components/BlockList/BlockList.test.tsx | 34 -
.../src/components/BlockList/BlockList.tsx | 19 -
.../__snapshots__/BlockList.test.tsx.snap | 36 -
frontend/src/components/BlockList/index.ts | 7 -
frontend/src/components/BrowserSession.tsx | 65 +-
.../Client/OAuth2ClientDetail.module.css | 14 -
.../components/Client/OAuth2ClientDetail.tsx | 64 +-
.../OAuth2ClientDetail.test.tsx.snap | 96 +--
frontend/src/components/CompatSession.tsx | 65 +-
frontend/src/components/GenericError.tsx | 6 +-
frontend/src/components/OAuth2Session.tsx | 51 +-
.../Session/EndBrowserSessionButton.tsx | 128 ++++
.../Session/EndCompatSessionButton.tsx | 89 +++
.../Session/EndOAuth2SessionButton.tsx | 115 +++
.../Session/EndSessionButton.stories.tsx | 39 -
.../components/Session/EndSessionButton.tsx | 36 +-
.../SessionCard/SessionCard.module.css | 6 +
.../BrowserSessionDetail.module.css | 10 -
.../SessionDetail/BrowserSessionDetail.tsx | 103 ++-
.../CompatSessionDetail.test.tsx | 6 +-
.../SessionDetail/CompatSessionDetail.tsx | 181 +++--
.../OAuth2SessionDetail.test.tsx | 4 +-
.../SessionDetail/OAuth2SessionDetail.tsx | 216 +++---
.../SessionDetail/SessionDetails.module.css | 33 -
.../SessionDetail/SessionDetails.tsx | 156 ----
.../components/SessionDetail/SessionInfo.tsx | 183 +++++
.../CompatSessionDetail.test.tsx.snap | 658 +++++++++--------
.../OAuth2SessionDetail.test.tsx.snap | 516 +++++++-------
.../VisualList/VisualList.module.css | 28 -
.../src/components/VisualList/VisualList.tsx | 40 --
.../__snapshots__/CompatSession.test.tsx.snap | 4 +-
.../__snapshots__/OAuth2Session.test.tsx.snap | 4 +-
frontend/src/gql/gql.ts | 114 +--
frontend/src/gql/graphql.ts | 672 ++++++++++++------
frontend/src/routeTree.gen.ts | 55 +-
frontend/src/routes/_account.index.lazy.tsx | 120 +++-
frontend/src/routes/_account.index.tsx | 11 +-
frontend/src/routes/_account.lazy.tsx | 14 +-
.../_account.sessions.browsers.lazy.tsx | 5 +-
.../routes/_account.sessions.index.lazy.tsx | 5 +-
frontend/src/routes/_account.tsx | 11 +-
.../src/routes/password.change.index.lazy.tsx | 5 +-
.../routes/password.change.success.lazy.tsx | 6 +-
.../routes/password.recovery.index.lazy.tsx | 6 +-
.../src/routes/reset-cross-signing.index.tsx | 33 +-
frontend/src/routes/reset-cross-signing.tsx | 5 +-
...ons.$id.lazy.tsx => sessions.$id.lazy.tsx} | 47 +-
...ount.sessions.$id.tsx => sessions.$id.tsx} | 2 +-
frontend/src/utils/simplifyUrl.ts | 33 +
frontend/stories/routes/index.stories.tsx | 11 +-
frontend/tests/mocks/handlers.ts | 37 +-
.../reset-cross-signing.test.tsx.snap | 104 ++-
.../account/__snapshots__/index.test.tsx.snap | 128 ++--
62 files changed, 2449 insertions(+), 2229 deletions(-)
delete mode 100644 frontend/src/components/Block/Block.module.css
delete mode 100644 frontend/src/components/Block/Block.stories.tsx
delete mode 100644 frontend/src/components/Block/Block.test.tsx
delete mode 100644 frontend/src/components/Block/Block.tsx
delete mode 100644 frontend/src/components/Block/__snapshots__/Block.test.tsx.snap
delete mode 100644 frontend/src/components/Block/index.ts
delete mode 100644 frontend/src/components/BlockList/BlockList.module.css
delete mode 100644 frontend/src/components/BlockList/BlockList.stories.tsx
delete mode 100644 frontend/src/components/BlockList/BlockList.test.tsx
delete mode 100644 frontend/src/components/BlockList/BlockList.tsx
delete mode 100644 frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap
delete mode 100644 frontend/src/components/BlockList/index.ts
delete mode 100644 frontend/src/components/Client/OAuth2ClientDetail.module.css
create mode 100644 frontend/src/components/Session/EndBrowserSessionButton.tsx
create mode 100644 frontend/src/components/Session/EndCompatSessionButton.tsx
create mode 100644 frontend/src/components/Session/EndOAuth2SessionButton.tsx
delete mode 100644 frontend/src/components/Session/EndSessionButton.stories.tsx
delete mode 100644 frontend/src/components/SessionDetail/BrowserSessionDetail.module.css
delete mode 100644 frontend/src/components/SessionDetail/SessionDetails.module.css
delete mode 100644 frontend/src/components/SessionDetail/SessionDetails.tsx
create mode 100644 frontend/src/components/SessionDetail/SessionInfo.tsx
delete mode 100644 frontend/src/components/VisualList/VisualList.module.css
delete mode 100644 frontend/src/components/VisualList/VisualList.tsx
rename frontend/src/routes/{_account.sessions.$id.lazy.tsx => sessions.$id.lazy.tsx} (61%)
rename frontend/src/routes/{_account.sessions.$id.tsx => sessions.$id.tsx} (93%)
create mode 100644 frontend/src/utils/simplifyUrl.ts
diff --git a/frontend/locales/en.json b/frontend/locales/en.json
index 72df9d5fe..f1ac98fe2 100644
--- a/frontend/locales/en.json
+++ b/frontend/locales/en.json
@@ -10,6 +10,7 @@
"expand": "Expand",
"save": "Save",
"save_and_continue": "Save and continue",
+ "sign_out": "Sign out",
"start_over": "Start over"
},
"branding": {
@@ -69,8 +70,7 @@
},
"compat_session_detail": {
"client_details_title": "Client info",
- "name": "Name",
- "session_details_title": "Session"
+ "name": "Name"
},
"device_type_icon_label": {
"mobile": "Mobile",
@@ -83,7 +83,7 @@
},
"end_session_button": {
"confirmation_modal_title": "Are you sure you want to end this session?",
- "text": "Sign out"
+ "text": "Remove device"
},
"error": {
"hideDetails": "Hide details",
@@ -233,6 +233,7 @@
"current": "Current",
"device_id_label": "Device ID",
"finished_label": "Finished",
+ "generic_browser_session": "Browser session",
"ip_label": "IP Address",
"last_active_label": "Last Active",
"name_for_platform": "{{name}} for {{platform}}",
diff --git a/frontend/src/components/Block/Block.module.css b/frontend/src/components/Block/Block.module.css
deleted file mode 100644
index 5399077ae..000000000
--- a/frontend/src/components/Block/Block.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.block {
- width: 100%;
- color: var(--cpd-color-text-primary);
- padding-bottom: var(--cpd-space-5x);
-
- &:last-child {
- border-bottom: none;
- }
-}
-
-.title {
- padding-bottom: var(--cpd-space-2x);
- border-bottom: var(--cpd-border-width-2) solid var(--cpd-color-gray-400);
-
- /* Workaround compound design tokens heading style being broken */
- font-weight: var(--cpd-font-weight-semibold) !important;
- font-size: var(--cpd-font-size-heading-sm) !important;
-}
diff --git a/frontend/src/components/Block/Block.stories.tsx b/frontend/src/components/Block/Block.stories.tsx
deleted file mode 100644
index b0dd5e5ae..000000000
--- a/frontend/src/components/Block/Block.stories.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import type { Meta, StoryObj } from "@storybook/react";
-import { Body, H1, H5 } from "@vector-im/compound-web";
-
-import Block from "./Block";
-
-const meta = {
- title: "UI/Block",
- component: Block,
- tags: ["autodocs"],
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Basic: Story = {
- render: (args) => (
-
- Title
- Subtitle
-
- Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit
- enim labore culpa sint ad nisi Lorem pariatur mollit ex esse
- exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit
- nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor
- minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure
- elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor
- Lorem duis laboris cupidatat officia voluptate. Culpa proident
- adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod.
- Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim.
- Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa
- et culpa duis.
-
-
- ),
-};
diff --git a/frontend/src/components/Block/Block.test.tsx b/frontend/src/components/Block/Block.test.tsx
deleted file mode 100644
index 95d22f7db..000000000
--- a/frontend/src/components/Block/Block.test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-// @vitest-environment happy-dom
-
-import { describe, expect, it } from "vitest";
-
-import render from "../../test-utils/render";
-import Block from "./Block";
-
-describe("Block", () => {
- it("render ", () => {
- const { asFragment } = render( );
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("render with children", () => {
- const { asFragment } = render(
-
- Title
- Body
- ,
- );
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("passes down the className prop", () => {
- const { asFragment } = render( );
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("renders with highlight", () => {
- const { asFragment } = render( );
- expect(asFragment()).toMatchSnapshot();
- });
-});
diff --git a/frontend/src/components/Block/Block.tsx b/frontend/src/components/Block/Block.tsx
deleted file mode 100644
index 0062da1b1..000000000
--- a/frontend/src/components/Block/Block.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import { Heading } from "@vector-im/compound-web";
-import cx from "classnames";
-import type { ReactNode } from "react";
-
-import styles from "./Block.module.css";
-
-type Props = React.PropsWithChildren<{
- title?: ReactNode;
- className?: string;
- highlight?: boolean;
-}>;
-
-const Block: React.FC = ({ children, className, highlight, title }) => {
- return (
-
- {title && (
-
- {title}
-
- )}
-
- {children}
-
- );
-};
-
-export default Block;
diff --git a/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap b/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap
deleted file mode 100644
index b48bcbd4c..000000000
--- a/frontend/src/components/Block/__snapshots__/Block.test.tsx.snap
+++ /dev/null
@@ -1,41 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`Block > passes down the className prop 1`] = `
-
-
-
-`;
-
-exports[`Block > render 1`] = `
-
-
-
-`;
-
-exports[`Block > render with children 1`] = `
-
-
-
-`;
-
-exports[`Block > renders with highlight 1`] = `
-
-
-
-`;
diff --git a/frontend/src/components/Block/index.ts b/frontend/src/components/Block/index.ts
deleted file mode 100644
index 8cf98ec84..000000000
--- a/frontend/src/components/Block/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-export { default } from "./Block";
diff --git a/frontend/src/components/BlockList/BlockList.module.css b/frontend/src/components/BlockList/BlockList.module.css
deleted file mode 100644
index e9fe11643..000000000
--- a/frontend/src/components/BlockList/BlockList.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.block-list {
- display: flex;
- flex-direction: column;
- align-content: flex-start;
- gap: var(--cpd-space-6x);
-}
diff --git a/frontend/src/components/BlockList/BlockList.stories.tsx b/frontend/src/components/BlockList/BlockList.stories.tsx
deleted file mode 100644
index 8f965331d..000000000
--- a/frontend/src/components/BlockList/BlockList.stories.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import type { Meta, StoryObj } from "@storybook/react";
-import { H2, Text } from "@vector-im/compound-web";
-
-import Block from "../Block";
-
-import BlockList from "./BlockList";
-
-const meta = {
- title: "UI/Block List",
- component: BlockList,
-} satisfies Meta;
-
-export default meta;
-
-type Story = StoryObj;
-
-export const Basic: Story = {
- render: (args) => (
-
-
- Block 1
- Body 1
-
-
- Block 2
- Body 2
-
-
- ),
-};
diff --git a/frontend/src/components/BlockList/BlockList.test.tsx b/frontend/src/components/BlockList/BlockList.test.tsx
deleted file mode 100644
index 0a470d5eb..000000000
--- a/frontend/src/components/BlockList/BlockList.test.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-// @vitest-environment happy-dom
-
-import { describe, expect, it } from "vitest";
-import render from "../../test-utils/render";
-import Block from "../Block";
-import BlockList from "./BlockList";
-
-describe("BlockList", () => {
- it("render an empty ", () => {
- const { asFragment } = render( );
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("render with children", () => {
- const { asFragment } = render(
-
- Block 1
- Block 2
- ,
- );
- expect(asFragment()).toMatchSnapshot();
- });
-
- it("passes down the className prop", () => {
- const { asFragment } = render( );
- expect(asFragment()).toMatchSnapshot();
- });
-});
diff --git a/frontend/src/components/BlockList/BlockList.tsx b/frontend/src/components/BlockList/BlockList.tsx
deleted file mode 100644
index cac68d945..000000000
--- a/frontend/src/components/BlockList/BlockList.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import cx from "classnames";
-
-import styles from "./BlockList.module.css";
-
-type Props = React.PropsWithChildren<{
- className?: string;
-}>;
-
-const BlockList: React.FC = ({ className, children }) => {
- return {children}
;
-};
-
-export default BlockList;
diff --git a/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap b/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap
deleted file mode 100644
index 69d5a7a86..000000000
--- a/frontend/src/components/BlockList/__snapshots__/BlockList.test.tsx.snap
+++ /dev/null
@@ -1,36 +0,0 @@
-// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-
-exports[`BlockList > passes down the className prop 1`] = `
-
-
-
-`;
-
-exports[`BlockList > render with children 1`] = `
-
-
-
- Block 1
-
-
- Block 2
-
-
-
-`;
-
-exports[`BlockList > render an empty 1`] = `
-
-
-
-`;
diff --git a/frontend/src/components/BlockList/index.ts b/frontend/src/components/BlockList/index.ts
deleted file mode 100644
index 46df82ca5..000000000
--- a/frontend/src/components/BlockList/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-export { default } from "./BlockList";
diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx
index 45808eead..3b2e7a58a 100644
--- a/frontend/src/components/BrowserSession.tsx
+++ b/frontend/src/components/BrowserSession.tsx
@@ -7,15 +7,12 @@
import IconChrome from "@browser-logos/chrome/chrome_64x64.png?url";
import IconFirefox from "@browser-logos/firefox/firefox_64x64.png?url";
import IconSafari from "@browser-logos/safari/safari_64x64.png?url";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Badge } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
-import { useCallback } from "react";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../gql";
-import { graphqlRequest } from "../graphql";
import DateTime from "./DateTime";
-import EndSessionButton from "./Session/EndSessionButton";
+import EndBrowserSessionButton from "./Session/EndBrowserSessionButton";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
@@ -24,62 +21,17 @@ const FRAGMENT = graphql(/* GraphQL */ `
id
createdAt
finishedAt
+ ...EndBrowserSessionButton_session
userAgent {
- raw
+ deviceType
name
os
model
- deviceType
}
- lastActiveIp
lastActiveAt
- lastAuthentication {
- id
- createdAt
- }
}
`);
-const END_SESSION_MUTATION = graphql(/* GraphQL */ `
- mutation EndBrowserSession($id: ID!) {
- endBrowserSession(input: { browserSessionId: $id }) {
- status
- browserSession {
- id
- ...BrowserSession_session
- }
- }
- }
-`);
-
-export const useEndBrowserSession = (
- sessionId: string,
- isCurrent: boolean,
-): (() => Promise) => {
- const queryClient = useQueryClient();
- const endSession = useMutation({
- mutationFn: (id: string) =>
- graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
- queryClient.invalidateQueries({ queryKey: ["browserSessionList"] });
- queryClient.invalidateQueries({
- queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id],
- });
-
- if (isCurrent) {
- window.location.reload();
- }
- },
- });
-
- const onSessionEnd = useCallback(async (): Promise => {
- await endSession.mutateAsync(sessionId);
- }, [endSession.mutateAsync, sessionId]);
-
- return onSessionEnd;
-};
-
export const browserLogoUri = (browser?: string): string | undefined => {
const lcBrowser = browser?.toLowerCase();
@@ -105,8 +57,6 @@ const BrowserSession: React.FC = ({ session, isCurrent }) => {
const data = useFragment(FRAGMENT, session);
const { t } = useTranslation();
- const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
-
const deviceType = data.userAgent?.deviceType ?? "UNKNOWN";
let deviceName: string | null = null;
@@ -175,14 +125,7 @@ const BrowserSession: React.FC = ({ session, isCurrent }) => {
{!data.finishedAt && (
-
-
-
-
- {clientName && }
-
-
-
+
)}
diff --git a/frontend/src/components/Client/OAuth2ClientDetail.module.css b/frontend/src/components/Client/OAuth2ClientDetail.module.css
deleted file mode 100644
index 52af4eeb4..000000000
--- a/frontend/src/components/Client/OAuth2ClientDetail.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.header {
- display: flex;
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- gap: var(--cpd-space-2x);
-}
diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx
index dac8c51ca..1c8d019c2 100644
--- a/frontend/src/components/Client/OAuth2ClientDetail.tsx
+++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx
@@ -1,4 +1,4 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -9,12 +9,9 @@ import { useTranslation } from "react-i18next";
import { type FragmentType, useFragment } from "../../gql";
import { graphql } from "../../gql/gql";
-import BlockList from "../BlockList/BlockList";
import ExternalLink from "../ExternalLink/ExternalLink";
import ClientAvatar from "../Session/ClientAvatar";
-import SessionDetails from "../SessionDetail/SessionDetails";
-
-import styles from "./OAuth2ClientDetail.module.css";
+import * as Info from "../SessionDetail/SessionInfo";
export const OAUTH2_CLIENT_FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Client_detail on Oauth2Client {
@@ -47,21 +44,9 @@ const OAuth2ClientDetail: React.FC = ({ client }) => {
const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client);
const { t } = useTranslation();
- const details = [
- { label: t("frontend.oauth2_client_detail.name"), value: data.clientName },
- {
- label: t("frontend.oauth2_client_detail.terms"),
- value: data.tosUri && ,
- },
- {
- label: t("frontend.oauth2_client_detail.policy"),
- value: data.policyUri && ,
- },
- ].filter(({ value }) => !!value);
-
return (
-
-
+
+
= ({ client }) => {
/>
{data.clientName}
-
-
+
+
+ {t("frontend.oauth2_client_detail.details_title")}
+
+
+ {data.clientName && (
+
+
+ {t("frontend.oauth2_client_detail.name")}
+
+ {data.clientName}
+
+ )}
+ {data.tosUri && (
+
+
+ {t("frontend.oauth2_client_detail.terms")}
+
+
+
+
+
+ )}
+ {data.policyUri && (
+
+
+ {t("frontend.oauth2_client_detail.policy")}
+
+
+
+
+
+ )}
+
+
+
);
};
diff --git a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap
index 833e5ae68..214083888 100644
--- a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap
+++ b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap
@@ -3,10 +3,10 @@
exports[` > renders client details 1`] = `
`;
diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx
index 16b732d4d..2ea3fdd60 100644
--- a/frontend/src/components/CompatSession.tsx
+++ b/frontend/src/components/CompatSession.tsx
@@ -1,17 +1,16 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
-import { useMutation, useQueryClient } from "@tanstack/react-query";
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../gql";
-import { graphqlRequest } from "../graphql";
+import simplifyUrl from "../utils/simplifyUrl";
import { browserLogoUri } from "./BrowserSession";
import DateTime from "./DateTime";
-import EndSessionButton from "./Session/EndSessionButton";
+import EndCompatSessionButton from "./Session/EndCompatSessionButton";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
@@ -23,6 +22,7 @@ export const FRAGMENT = graphql(/* GraphQL */ `
finishedAt
lastActiveIp
lastActiveAt
+ ...EndCompatSessionButton_session
userAgent {
name
os
@@ -36,59 +36,11 @@ export const FRAGMENT = graphql(/* GraphQL */ `
}
`);
-export const END_SESSION_MUTATION = graphql(/* GraphQL */ `
- mutation EndCompatSession($id: ID!) {
- endCompatSession(input: { compatSessionId: $id }) {
- status
- compatSession {
- id
- }
- }
- }
-`);
-
-export const simplifyUrl = (url: string): string => {
- let parsed: URL;
- try {
- parsed = new URL(url);
- } catch (_e) {
- // Not a valid URL, return the original
- return url;
- }
-
- // Clear out the search params and hash
- parsed.search = "";
- parsed.hash = "";
-
- if (parsed.protocol === "https:") {
- return parsed.hostname;
- }
-
- // Return the simplified URL
- return parsed.toString();
-};
-
const CompatSession: React.FC<{
session: FragmentType;
}> = ({ session }) => {
const { t } = useTranslation();
const data = useFragment(FRAGMENT, session);
- const queryClient = useQueryClient();
- const endSession = useMutation({
- mutationFn: (id: string) =>
- graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
- queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
- queryClient.invalidateQueries({
- queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id],
- });
- },
- });
-
- const onSessionEnd = async (): Promise => {
- await endSession.mutateAsync(data.id);
- };
const clientName = data.ssoLogin?.redirectUri
? simplifyUrl(data.ssoLogin.redirectUri)
@@ -146,14 +98,7 @@ const CompatSession: React.FC<{
{!data.finishedAt && (
-
-
-
-
- {clientName && }
-
-
-
+
)}
diff --git a/frontend/src/components/GenericError.tsx b/frontend/src/components/GenericError.tsx
index 815477761..927a97921 100644
--- a/frontend/src/components/GenericError.tsx
+++ b/frontend/src/components/GenericError.tsx
@@ -8,8 +8,6 @@ import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error"
import { Button } from "@vector-im/compound-web";
import { useState } from "react";
import { Translation } from "react-i18next";
-
-import BlockList from "./BlockList";
import styles from "./GenericError.module.css";
import PageHeading from "./PageHeading";
@@ -21,7 +19,7 @@ const GenericError: React.FC<{ error: unknown; dontSuspend?: boolean }> = ({
return (
{(t) => (
-
+
= ({
{String(error)}
)}
-
+
)}
);
diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx
index 5652a84fc..cc92f26c6 100644
--- a/frontend/src/components/OAuth2Session.tsx
+++ b/frontend/src/components/OAuth2Session.tsx
@@ -1,18 +1,10 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import { useMutation, useQueryClient } from "@tanstack/react-query";
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../gql";
import type { DeviceType, Oauth2ApplicationType } from "../gql/graphql";
-import { graphqlRequest } from "../graphql";
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
import DateTime from "./DateTime";
-import EndSessionButton from "./Session/EndSessionButton";
+import EndOAuth2SessionButton from "./Session/EndOAuth2SessionButton";
import LastActive from "./Session/LastActive";
import * as Card from "./SessionCard";
@@ -25,6 +17,8 @@ export const FRAGMENT = graphql(/* GraphQL */ `
lastActiveIp
lastActiveAt
+ ...EndOAuth2SessionButton_session
+
userAgent {
name
model
@@ -42,17 +36,6 @@ export const FRAGMENT = graphql(/* GraphQL */ `
}
`);
-export const END_SESSION_MUTATION = graphql(/* GraphQL */ `
- mutation EndOAuth2Session($id: ID!) {
- endOauth2Session(input: { oauth2SessionId: $id }) {
- status
- oauth2Session {
- id
- }
- }
- }
-`);
-
const getDeviceTypeFromClientAppType = (
appType?: Oauth2ApplicationType | null,
): DeviceType => {
@@ -72,22 +55,6 @@ type Props = {
const OAuth2Session: React.FC = ({ session }) => {
const { t } = useTranslation();
const data = useFragment(FRAGMENT, session);
- const queryClient = useQueryClient();
- const endSession = useMutation({
- mutationFn: (id: string) =>
- graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
- queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
- queryClient.invalidateQueries({
- queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id],
- });
- },
- });
-
- const onSessionEnd = async (): Promise => {
- await endSession.mutateAsync(data.id);
- };
const deviceId = getDeviceIdFromScope(data.scope);
@@ -149,17 +116,7 @@ const OAuth2Session: React.FC = ({ session }) => {
{!data.finishedAt && (
-
-
-
-
-
-
-
-
+
)}
diff --git a/frontend/src/components/Session/EndBrowserSessionButton.tsx b/frontend/src/components/Session/EndBrowserSessionButton.tsx
new file mode 100644
index 000000000..f04a9298c
--- /dev/null
+++ b/frontend/src/components/Session/EndBrowserSessionButton.tsx
@@ -0,0 +1,128 @@
+// Copyright 2025 New Vector Ltd.
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+// Please see LICENSE in the repository root for full details.
+
+import {
+ type UseMutationResult,
+ useMutation,
+ useQueryClient,
+} from "@tanstack/react-query";
+import { useTranslation } from "react-i18next";
+import { type FragmentType, graphql, useFragment } from "../../gql";
+import { graphqlRequest } from "../../graphql";
+import * as Card from "../SessionCard";
+import EndSessionButton from "./EndSessionButton";
+
+const FRAGMENT = graphql(/* GraphQL */ `
+ fragment EndBrowserSessionButton_session on BrowserSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ }
+`);
+
+const END_SESSION_MUTATION = graphql(/* GraphQL */ `
+ mutation EndBrowserSession($id: ID!) {
+ endBrowserSession(input: { browserSessionId: $id }) {
+ status
+ browserSession {
+ id
+ }
+ }
+ }
+`);
+
+export const useEndBrowserSession = (
+ sessionId: string,
+ isCurrent: boolean,
+): UseMutationResult => {
+ const queryClient = useQueryClient();
+ const endSession = useMutation({
+ mutationFn: () =>
+ graphqlRequest({
+ query: END_SESSION_MUTATION,
+ variables: { id: sessionId },
+ }),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
+ queryClient.invalidateQueries({ queryKey: ["browserSessionList"] });
+ queryClient.invalidateQueries({
+ queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id],
+ });
+
+ if (isCurrent) {
+ window.location.reload();
+ }
+ },
+ });
+
+ return endSession;
+};
+
+type Props = {
+ session: FragmentType;
+ size: "sm" | "lg";
+};
+
+const EndBrowserSessionButton: React.FC = ({ session, size }) => {
+ const { t } = useTranslation();
+ const data = useFragment(FRAGMENT, session);
+ const queryClient = useQueryClient();
+ const endSession = useMutation({
+ mutationFn: () =>
+ graphqlRequest({
+ query: END_SESSION_MUTATION,
+ variables: { id: data.id },
+ }),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
+ queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
+ queryClient.invalidateQueries({
+ queryKey: ["sessionDetail", data.endBrowserSession.browserSession?.id],
+ });
+ },
+ });
+
+ const deviceType = data.userAgent?.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;
+ }
+
+ return (
+
+
+
+
+ {clientName && }
+
+
+
+ );
+};
+
+export default EndBrowserSessionButton;
diff --git a/frontend/src/components/Session/EndCompatSessionButton.tsx b/frontend/src/components/Session/EndCompatSessionButton.tsx
new file mode 100644
index 000000000..68f67b91b
--- /dev/null
+++ b/frontend/src/components/Session/EndCompatSessionButton.tsx
@@ -0,0 +1,89 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useTranslation } from "react-i18next";
+import { type FragmentType, graphql, useFragment } from "../../gql";
+import { graphqlRequest } from "../../graphql";
+import simplifyUrl from "../../utils/simplifyUrl";
+import * as Card from "../SessionCard";
+import EndSessionButton from "./EndSessionButton";
+
+const FRAGMENT = graphql(/* GraphQL */ `
+ fragment EndCompatSessionButton_session on CompatSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+ }
+`);
+
+const END_SESSION_MUTATION = graphql(/* GraphQL */ `
+ mutation EndCompatSession($id: ID!) {
+ endCompatSession(input: { compatSessionId: $id }) {
+ status
+ compatSession {
+ id
+ }
+ }
+ }
+`);
+
+type Props = {
+ session: FragmentType;
+ size: "sm" | "lg";
+};
+
+const EndCompatSessionButton: React.FC = ({ session, size }) => {
+ const { t } = useTranslation();
+ const data = useFragment(FRAGMENT, session);
+ const queryClient = useQueryClient();
+ const endSession = useMutation({
+ mutationFn: () =>
+ graphqlRequest({
+ query: END_SESSION_MUTATION,
+ variables: { id: data.id },
+ }),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
+ queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
+ queryClient.invalidateQueries({
+ queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id],
+ });
+ },
+ });
+
+ const clientName = data.ssoLogin?.redirectUri
+ ? simplifyUrl(data.ssoLogin.redirectUri)
+ : undefined;
+
+ const deviceType = data.userAgent?.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"));
+
+ return (
+
+
+
+
+ {clientName && }
+
+
+
+ );
+};
+
+export default EndCompatSessionButton;
diff --git a/frontend/src/components/Session/EndOAuth2SessionButton.tsx b/frontend/src/components/Session/EndOAuth2SessionButton.tsx
new file mode 100644
index 000000000..245ddad12
--- /dev/null
+++ b/frontend/src/components/Session/EndOAuth2SessionButton.tsx
@@ -0,0 +1,115 @@
+// Copyright 2025 New Vector Ltd.
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+// Please see LICENSE in the repository root for full details.
+
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useTranslation } from "react-i18next";
+import { type FragmentType, graphql, useFragment } from "../../gql";
+import type { DeviceType, Oauth2ApplicationType } from "../../gql/graphql";
+import { graphqlRequest } from "../../graphql";
+import * as Card from "../SessionCard";
+import EndSessionButton from "./EndSessionButton";
+
+const FRAGMENT = graphql(/* GraphQL */ `
+ fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
+ }
+`);
+
+const END_SESSION_MUTATION = graphql(/* GraphQL */ `
+ mutation EndOAuth2Session($id: ID!) {
+ endOauth2Session(input: { oauth2SessionId: $id }) {
+ status
+ oauth2Session {
+ id
+ }
+ }
+ }
+`);
+
+const getDeviceTypeFromClientAppType = (
+ appType?: Oauth2ApplicationType | null,
+): DeviceType => {
+ if (appType === "WEB") {
+ return "PC";
+ }
+ if (appType === "NATIVE") {
+ return "MOBILE";
+ }
+ return "UNKNOWN";
+};
+
+type Props = {
+ session: FragmentType;
+ size: "sm" | "lg";
+};
+
+const EndOAuth2SessionButton: React.FC = ({ session, size }) => {
+ const { t } = useTranslation();
+ const data = useFragment(FRAGMENT, session);
+ const queryClient = useQueryClient();
+ const endSession = useMutation({
+ mutationFn: () =>
+ graphqlRequest({
+ query: END_SESSION_MUTATION,
+ variables: { id: data.id },
+ }),
+ onSuccess: (data) => {
+ queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
+ queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
+ queryClient.invalidateQueries({
+ queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id],
+ });
+ },
+ });
+
+ const deviceType =
+ (data.userAgent?.deviceType === "UNKNOWN"
+ ? null
+ : data.userAgent?.deviceType) ??
+ getDeviceTypeFromClientAppType(data.client.applicationType);
+
+ const clientName = data.client.clientName || data.client.clientId;
+
+ 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 (
+
+
+
+
+
+
+
+
+ );
+};
+
+export default EndOAuth2SessionButton;
diff --git a/frontend/src/components/Session/EndSessionButton.stories.tsx b/frontend/src/components/Session/EndSessionButton.stories.tsx
deleted file mode 100644
index 838f51a27..000000000
--- a/frontend/src/components/Session/EndSessionButton.stories.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import { action } from "@storybook/addon-actions";
-import type { Meta, StoryObj } from "@storybook/react";
-
-import EndSessionButton from "./EndSessionButton";
-
-const endSession = action("end-session");
-
-const meta = {
- title: "UI/Session/End Session Button",
- component: EndSessionButton,
- tags: ["autodocs"],
- args: {
- endSession: async (): Promise => {
- await new Promise((resolve) => setTimeout(resolve, 300));
- endSession();
- },
- },
- argTypes: {
- children: { control: "text" },
- },
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Basic: Story = {};
-
-export const WithChildren: Story = {
- args: {
- children:
- "Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.",
- },
-};
diff --git a/frontend/src/components/Session/EndSessionButton.tsx b/frontend/src/components/Session/EndSessionButton.tsx
index 54a54853c..4b311066f 100644
--- a/frontend/src/components/Session/EndSessionButton.tsx
+++ b/frontend/src/components/Session/EndSessionButton.tsx
@@ -1,14 +1,14 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
-import IconSignOut from "@vector-im/compound-design-tokens/assets/web/icons/sign-out";
+import type { UseMutationResult } from "@tanstack/react-query";
+import IconDelete from "@vector-im/compound-design-tokens/assets/web/icons/delete";
import { Button } from "@vector-im/compound-web";
import { useState } from "react";
import { useTranslation } from "react-i18next";
-
import * as Dialog from "../Dialog";
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
@@ -17,25 +17,17 @@ import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
* Handles loading state while endSession is in progress
*/
const EndSessionButton: React.FC<
- React.PropsWithChildren<{ endSession: () => Promise }>
-> = ({ children, endSession }) => {
- const [inProgress, setInProgress] = useState(false);
+ React.PropsWithChildren<{
+ mutation: UseMutationResult;
+ size: "sm" | "lg";
+ }>
+> = ({ children, mutation, size }) => {
const [open, setOpen] = useState(false);
const { t } = useTranslation();
- const onConfirm = async (
- e: React.MouseEvent,
- ): Promise => {
+ const onConfirm = (e: React.MouseEvent): void => {
e.preventDefault();
-
- setInProgress(true);
- try {
- await endSession();
- setOpen(false);
- } catch (error) {
- console.error("Failed to end session", error);
- }
- setInProgress(false);
+ mutation.mutate(void 0, { onSuccess: () => setOpen(false) });
};
return (
@@ -43,7 +35,7 @@ const EndSessionButton: React.FC<
open={open}
onOpenChange={setOpen}
trigger={
-
+
{t("frontend.end_session_button.text")}
}
@@ -59,10 +51,10 @@ const EndSessionButton: React.FC<
kind="primary"
destructive
onClick={onConfirm}
- disabled={inProgress}
- Icon={inProgress ? undefined : IconSignOut}
+ disabled={mutation.isPending}
+ Icon={mutation.isPending ? undefined : IconDelete}
>
- {inProgress && }
+ {mutation.isPending && }
{t("frontend.end_session_button.text")}
diff --git a/frontend/src/components/SessionCard/SessionCard.module.css b/frontend/src/components/SessionCard/SessionCard.module.css
index b36c3819d..ce3f02052 100644
--- a/frontend/src/components/SessionCard/SessionCard.module.css
+++ b/frontend/src/components/SessionCard/SessionCard.module.css
@@ -111,6 +111,10 @@
flex-wrap: wrap;
gap: var(--cpd-space-4x) var(--cpd-space-10x);
+ & > * {
+ min-width: 0;
+ }
+
& .key {
font: var(--cpd-font-body-sm-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
@@ -121,6 +125,8 @@
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
color: var(--cpd-color-text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
}
}
}
diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css b/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css
deleted file mode 100644
index d14155405..000000000
--- a/frontend/src/components/SessionDetail/BrowserSessionDetail.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.current-badge {
- align-self: flex-start;
-}
diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx
index f5009e829..2fe2db3f9 100644
--- a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx
+++ b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx
@@ -1,4 +1,4 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -7,22 +7,19 @@
import { Badge } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
-
import { type FragmentType, graphql, useFragment } from "../../gql";
-import BlockList from "../BlockList/BlockList";
-import { useEndBrowserSession } from "../BrowserSession";
import DateTime from "../DateTime";
-import EndSessionButton from "../Session/EndSessionButton";
-
-import styles from "./BrowserSessionDetail.module.css";
-import SessionDetails from "./SessionDetails";
+import EndBrowserSessionButton from "../Session/EndBrowserSessionButton";
+import LastActive from "../Session/LastActive";
import SessionHeader from "./SessionHeader";
+import * as Info from "./SessionInfo";
const FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_detail on BrowserSession {
id
createdAt
finishedAt
+ ...EndBrowserSessionButton_session
userAgent {
name
model
@@ -50,51 +47,79 @@ const BrowserSessionDetail: React.FC = ({ session, isCurrent }) => {
const data = useFragment(FRAGMENT, session);
const { t } = useTranslation();
- const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
-
- let sessionName = "Browser session";
+ let sessionName = t("frontend.session.generic_browser_session");
if (data.userAgent) {
if (data.userAgent.model && data.userAgent.name) {
- sessionName = `${data.userAgent.name} on ${data.userAgent.model}`;
+ sessionName = t("frontend.session.name_for_platform", {
+ name: data.userAgent.name,
+ platform: data.userAgent.model,
+ });
} else if (data.userAgent.name && data.userAgent.os) {
- sessionName = `${data.userAgent.name} on ${data.userAgent.os}`;
+ sessionName = t("frontend.session.name_for_platform", {
+ name: data.userAgent.name,
+ platform: data.userAgent.os,
+ });
} else if (data.userAgent.name) {
sessionName = data.userAgent.name;
}
}
- const finishedAt = data.finishedAt
- ? [
- {
- label: t("frontend.session.finished_label"),
- value: ,
- },
- ]
- : [];
-
- const sessionDetails = [...finishedAt];
-
return (
-
+
{isCurrent && (
-
+
{t("frontend.browser_session_details.current_badge")}
)}
{sessionName}
-
- {!data.finishedAt && }
-
+
+
+ {t("frontend.session.title")}
+
+
+ {data.lastActiveAt && (
+
+
+ {t("frontend.session.last_active_label")}
+
+
+
+
+
+ )}
+
+
+
+ {t("frontend.session.signed_in_label")}
+
+
+
+
+
+
+ {data.finishedAt && (
+
+
+ {t("frontend.session.finished_label")}
+
+
+
+
+
+ )}
+
+ {data.lastActiveIp && (
+
+ {t("frontend.session.ip_label")}
+
+ {data.lastActiveIp}
+
+
+ )}
+
+
+ {!data.finishedAt && }
+
);
};
diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx
index c9bbc28d0..30fb72418 100644
--- a/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx
+++ b/frontend/src/components/SessionDetail/CompatSessionDetail.test.tsx
@@ -38,7 +38,7 @@ describe("", () => {
expect(container).toMatchSnapshot();
expect(queryByText("Finished")).toBeFalsy();
- expect(getByText("Sign out")).toBeTruthy();
+ expect(getByText("Remove device")).toBeTruthy();
});
it("renders a compatability session without an ssoLogin", () => {
@@ -56,7 +56,7 @@ describe("", () => {
expect(container).toMatchSnapshot();
expect(queryByText("Finished")).toBeFalsy();
- expect(getByText("Sign out")).toBeTruthy();
+ expect(getByText("Remove device")).toBeTruthy();
});
it("renders a finished compatability session details", () => {
@@ -74,6 +74,6 @@ describe("", () => {
expect(container).toMatchSnapshot();
expect(getByText("Finished")).toBeTruthy();
- expect(queryByText("Sign out")).toBeFalsy();
+ expect(queryByText("Remove device")).toBeFalsy();
});
});
diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx
index 097c130d3..144e7ef37 100644
--- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx
+++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx
@@ -1,21 +1,19 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
-import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { VisualList } from "@vector-im/compound-web";
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../../gql";
-import { graphqlRequest } from "../../graphql";
-import BlockList from "../BlockList/BlockList";
-import { END_SESSION_MUTATION, simplifyUrl } from "../CompatSession";
+import simplifyUrl from "../../utils/simplifyUrl";
import DateTime from "../DateTime";
-import ExternalLink from "../ExternalLink/ExternalLink";
-import EndSessionButton from "../Session/EndSessionButton";
-import SessionDetails from "./SessionDetails";
+import EndCompatSessionButton from "../Session/EndCompatSessionButton";
+import LastActive from "../Session/LastActive";
import SessionHeader from "./SessionHeader";
+import * as Info from "./SessionInfo";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSession_detail on CompatSession {
@@ -25,11 +23,15 @@ export const FRAGMENT = graphql(/* GraphQL */ `
finishedAt
lastActiveIp
lastActiveAt
+
+ ...EndCompatSessionButton_session
+
userAgent {
name
os
model
}
+
ssoLogin {
id
redirectUri
@@ -43,74 +45,111 @@ type Props = {
const CompatSessionDetail: React.FC = ({ session }) => {
const data = useFragment(FRAGMENT, session);
- const queryClient = useQueryClient();
- const endSession = useMutation({
- mutationFn: (id: string) =>
- graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
- queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
- queryClient.invalidateQueries({
- queryKey: ["sessionDetail", data.endCompatSession.compatSession?.id],
- });
- },
- });
const { t } = useTranslation();
- const onSessionEnd = async (): Promise => {
- await endSession.mutateAsync(data.id);
- };
+ 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 finishedAt = data.finishedAt
- ? [
- {
- label: t("frontend.session.finished_label"),
- value: ,
- },
- ]
- : [];
-
- const sessionDetails = [...finishedAt];
-
- const clientDetails: { label: string; value: string | React.ReactElement }[] =
- [];
-
- if (data.ssoLogin?.redirectUri) {
- clientDetails.push({
- label: t("frontend.compat_session_detail.name"),
- value: data.userAgent?.name ?? simplifyUrl(data.ssoLogin.redirectUri),
- });
- clientDetails.push({
- label: t("frontend.session.uri_label"),
- value: (
-
- {data.ssoLogin?.redirectUri}
-
- ),
- });
- }
+ const clientName = data.ssoLogin?.redirectUri
+ ? simplifyUrl(data.ssoLogin.redirectUri)
+ : data.deviceId || data.id;
return (
-
- {data.deviceId || data.id}
-
- {clientDetails.length > 0 ? (
-
- ) : null}
- {!data.finishedAt && }
-
+
+
+ {clientName}: {deviceName}
+
+
+
+ {t("frontend.session.title")}
+
+
+ {data.lastActiveAt && (
+
+
+ {t("frontend.session.last_active_label")}
+
+
+
+
+
+ )}
+
+
+
+ {t("frontend.session.signed_in_label")}
+
+
+
+
+
+
+ {data.finishedAt && (
+
+
+ {t("frontend.session.finished_label")}
+
+
+
+
+
+ )}
+
+
+
+ {t("frontend.session.device_id_label")}
+
+ {data.deviceId}
+
+
+ {data.lastActiveIp && (
+
+ {t("frontend.session.ip_label")}
+
+ {data.lastActiveIp}
+
+
+ )}
+
+
+
+ {t("frontend.session.scopes_label")}
+
+
+
+
+
+
+
+
+
+
+ {t("frontend.compat_session_detail.client_details_title")}
+
+
+
+
+ {t("frontend.compat_session_detail.name")}
+
+ {deviceName}
+
+
+ {t("frontend.session.uri_label")}
+ {data.ssoLogin?.redirectUri}
+
+
+
+
+ {!data.finishedAt && }
+
);
};
diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx
index b0c30c7d0..7f33da2bb 100644
--- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx
+++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.test.tsx
@@ -44,7 +44,7 @@ describe("", () => {
expect(asFragment()).toMatchSnapshot();
expect(queryByText("Finished")).toBeFalsy();
- expect(getByText("Sign out")).toBeTruthy();
+ expect(getByText("Remove device")).toBeTruthy();
});
it("renders a finished session details", () => {
@@ -62,6 +62,6 @@ describe("", () => {
expect(asFragment()).toMatchSnapshot();
expect(getByText("Finished")).toBeTruthy();
- expect(queryByText("Sign out")).toBeFalsy();
+ expect(queryByText("Remove device")).toBeFalsy();
});
});
diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx
index 58d32eae3..656067cd0 100644
--- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx
+++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx
@@ -1,23 +1,19 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
-import { useMutation, useQueryClient } from "@tanstack/react-query";
import { parseISO } from "date-fns";
import { useTranslation } from "react-i18next";
import { type FragmentType, graphql, useFragment } from "../../gql";
-import { graphqlRequest } from "../../graphql";
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
-import BlockList from "../BlockList/BlockList";
import DateTime from "../DateTime";
-import { Link } from "../Link";
-import { END_SESSION_MUTATION } from "../OAuth2Session";
import ClientAvatar from "../Session/ClientAvatar";
-import EndSessionButton from "../Session/EndSessionButton";
-import SessionDetails from "./SessionDetails";
+import EndOAuth2SessionButton from "../Session/EndOAuth2SessionButton";
+import LastActive from "../Session/LastActive";
import SessionHeader from "./SessionHeader";
+import * as Info from "./SessionInfo";
export const FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Session_detail on Oauth2Session {
@@ -27,6 +23,15 @@ export const FRAGMENT = graphql(/* GraphQL */ `
finishedAt
lastActiveIp
lastActiveAt
+
+ ...EndOAuth2SessionButton_session
+
+ userAgent {
+ name
+ model
+ os
+ }
+
client {
id
clientId
@@ -43,90 +48,129 @@ type Props = {
const OAuth2SessionDetail: React.FC = ({ session }) => {
const data = useFragment(FRAGMENT, session);
- const queryClient = useQueryClient();
- const endSession = useMutation({
- mutationFn: (id: string) =>
- graphqlRequest({ query: END_SESSION_MUTATION, variables: { id } }),
- onSuccess: (data) => {
- queryClient.invalidateQueries({ queryKey: ["sessionsOverview"] });
- queryClient.invalidateQueries({ queryKey: ["appSessionList"] });
- queryClient.invalidateQueries({
- queryKey: ["sessionDetail", data.endOauth2Session.oauth2Session?.id],
- });
- },
- });
-
const { t } = useTranslation();
- const onSessionEnd = async (): Promise => {
- await endSession.mutateAsync(data.id);
- };
-
const deviceId = getDeviceIdFromScope(data.scope);
+ const clientName = data.client.clientName || data.client.clientId;
- const finishedAt = data.finishedAt
- ? [
- {
- label: t("frontend.session.finished_label"),
- value: ,
- },
- ]
- : [];
-
- const sessionDetails = [...finishedAt];
-
- const clientTitle = (
-
- {t("frontend.oauth2_session_detail.client_title")}
-
- );
- const clientDetails = [
- {
- label: t("frontend.oauth2_session_detail.client_details_name"),
- value: (
- <>
-
- {data.client.clientName}
- >
- ),
- },
- {
- label: t("frontend.session.client_id_label"),
- value: {data.client.clientId},
- },
- {
- label: t("frontend.session.uri_label"),
- value: (
-
- {data.client.clientUri}
-
- ),
- },
- ];
+ 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 (
-
- {deviceId || data.id}
-
-
- {!data.finishedAt && }
-
+
+
+ {clientName}: {deviceName}
+
+
+
+ {t("frontend.session.title")}
+
+
+ {data.lastActiveAt && (
+
+
+ {t("frontend.session.last_active_label")}
+
+
+
+
+
+ )}
+
+
+
+ {t("frontend.session.signed_in_label")}
+
+
+
+
+
+
+ {data.finishedAt && (
+
+
+ {t("frontend.session.finished_label")}
+
+
+
+
+
+ )}
+
+
+
+ {t("frontend.session.device_id_label")}
+
+ {deviceId}
+
+
+ {data.lastActiveIp && (
+
+ {t("frontend.session.ip_label")}
+
+ {data.lastActiveIp}
+
+
+ )}
+
+
+
+ {t("frontend.session.scopes_label")}
+
+
+
+
+
+
+ {t("frontend.oauth2_session_detail.client_title")}
+
+
+
+
+ {t("frontend.oauth2_session_detail.client_details_name")}
+
+
+
+ {data.client.clientName}
+
+
+
+
+ {t("frontend.session.client_id_label")}
+
+
+ {data.client.clientId}
+
+
+
+ {t("frontend.session.uri_label")}
+
+
+ {data.client.clientUri}
+
+
+
+
+
+
+ {!data.finishedAt &&
}
+
);
};
diff --git a/frontend/src/components/SessionDetail/SessionDetails.module.css b/frontend/src/components/SessionDetail/SessionDetails.module.css
deleted file mode 100644
index 5b3275a0d..000000000
--- a/frontend/src/components/SessionDetail/SessionDetails.module.css
+++ /dev/null
@@ -1,33 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.wrapper {
- display: flex;
- flex-wrap: wrap;
- gap: var(--cpd-space-4x);
- margin-bottom: var(--cpd-space-4x);
- margin-top: var(--cpd-space-8x);
-}
-
-.wrapper h5 {
- color: var(--cpd-color-text-secondary);
-}
-
-.wrapper .datum {
- width: max-content;
-}
-
-.datum {
- flex-grow: 1;
- max-width: 100%;
-}
-
-.datum-value {
- font-size: var(--cpd-font-size-body-md);
- text-overflow: ellipsis;
- overflow: hidden;
-}
diff --git a/frontend/src/components/SessionDetail/SessionDetails.tsx b/frontend/src/components/SessionDetail/SessionDetails.tsx
deleted file mode 100644
index 67430360b..000000000
--- a/frontend/src/components/SessionDetail/SessionDetails.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import IconChat from "@vector-im/compound-design-tokens/assets/web/icons/chat";
-import IconComputer from "@vector-im/compound-design-tokens/assets/web/icons/computer";
-import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error";
-import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info";
-import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send";
-import IconUserProfile from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
-import { Text } from "@vector-im/compound-web";
-import type { ReactNode } from "react";
-import { useTranslation } from "react-i18next";
-
-import Block from "../Block/Block";
-import DateTime from "../DateTime";
-import LastActive from "../Session/LastActive";
-import { VisualList, VisualListItem } from "../VisualList/VisualList";
-
-import styles from "./SessionDetails.module.css";
-
-type Detail = { label: string; value: ReactNode };
-type Props = {
- title: string | ReactNode;
- lastActive?: Date;
- signedIn?: Date;
- deviceId?: string | null;
- ipAddress?: string;
- scopes?: string[];
- details?: Detail[];
-};
-
-const Scope: React.FC<{ scope: string }> = ({ scope }) => {
- const { t } = useTranslation();
- // Filter out "urn:matrix:org.matrix.msc2967.client:device:"
- if (scope.startsWith("urn:matrix:org.matrix.msc2967.client:device:")) {
- return null;
- }
-
- // Needs to be manually kept in sync with /templates/components/scope.html
- const scopeMap: Record = {
- openid: [[0, IconUserProfile, t("mas.scope.view_profile")]],
- "urn:mas:graphql:*": [
- [1, IconInfo, t("mas.scope.edit_profile")],
- [2, IconComputer, t("mas.scope.manage_sessions")],
- ],
- "urn:matrix:org.matrix.msc2967.client:api:*": [
- [3, IconChat, t("mas.scope.view_messages")],
- [4, IconSend, t("mas.scope.send_messages")],
- ],
- "urn:synapse:admin:*": [[5, IconError, t("mas.scope.synapse_admin")]],
- "urn:mas:admin": [[6, IconError, t("mas.scope.mas_admin")]],
- } as const;
-
- const mappedScopes: [number | string, typeof IconInfo, string][] = scopeMap[
- scope
- ] ?? [[scope, IconInfo, scope]];
-
- return (
- <>
- {mappedScopes.map(([key, Icon, text]) => (
-
- ))}
- >
- );
-};
-
-const Datum: React.FC<{ label: string; value: ReactNode }> = ({
- label,
- value,
-}) => {
- return (
-
-
- {label}
-
- {typeof value === "string" ? (
-
- {value}
-
- ) : (
- value
- )}
-
- );
-};
-
-const SessionDetails: React.FC = ({
- title,
- lastActive,
- signedIn,
- deviceId,
- ipAddress,
- details,
- scopes,
-}) => {
- const { t } = useTranslation();
-
- return (
-
-
- {lastActive && (
-
- }
- />
- )}
- {signedIn && (
-
- }
- />
- )}
- {deviceId && (
-
- )}
- {ipAddress && (
- {ipAddress}}
- />
- )}
- {details?.map(({ label, value }) => (
-
- ))}
-
-
- {scopes?.length && (
-
- {scopes.map((scope) => (
-
- ))}
-
- }
- />
- )}
-
- );
-};
-
-export default SessionDetails;
diff --git a/frontend/src/components/SessionDetail/SessionInfo.tsx b/frontend/src/components/SessionDetail/SessionInfo.tsx
new file mode 100644
index 000000000..c6632f48f
--- /dev/null
+++ b/frontend/src/components/SessionDetail/SessionInfo.tsx
@@ -0,0 +1,183 @@
+// Copyright 2025 New Vector Ltd.
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+// Please see LICENSE in the repository root for full details.
+
+import IconChat from "@vector-im/compound-design-tokens/assets/web/icons/chat";
+import IconComputer from "@vector-im/compound-design-tokens/assets/web/icons/computer";
+import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error";
+import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info";
+import IconSend from "@vector-im/compound-design-tokens/assets/web/icons/send";
+import IconUserProfile from "@vector-im/compound-design-tokens/assets/web/icons/user-profile";
+import {
+ Heading,
+ Separator,
+ Text,
+ VisualList,
+ VisualListItem,
+} from "@vector-im/compound-web";
+import cx from "classnames";
+import type * as React from "react";
+import { useTranslation } from "react-i18next";
+
+export const ScopeViewProfile: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.view_profile")}
+
+ );
+};
+
+const ScopeEditProfile: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.edit_profile")}
+
+ );
+};
+
+const ScopeManageSessions: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.manage_sessions")}
+
+ );
+};
+
+export const ScopeViewMessages: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.view_messages")}
+
+ );
+};
+
+export const ScopeSendMessages: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.send_messages")}
+
+ );
+};
+
+const ScopeSynapseAdmin: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+
+ {t("mas.scope.synapse_admin")}
+
+ );
+};
+
+const ScopeMasAdmin: React.FC = () => {
+ const { t } = useTranslation();
+ return (
+ {t("mas.scope.mas_admin")}
+ );
+};
+
+const ScopeOther: React.FC<{ scope: string }> = ({ scope }) => {
+ return {scope} ;
+};
+
+const Scope: React.FC<{ scope: string }> = ({ scope }) => {
+ // Filter out "urn:matrix:org.matrix.msc2967.client:device:"
+ if (scope.startsWith("urn:matrix:org.matrix.msc2967.client:device:")) {
+ return null;
+ }
+
+ switch (scope) {
+ case "openid":
+ return ;
+ case "urn:mas:graphql:*":
+ return (
+ <>
+
+
+ >
+ );
+ case "urn:matrix:org.matrix.msc2967.client:api:*":
+ return (
+ <>
+
+
+ >
+ );
+ case "urn:synapse:admin:*":
+ return ;
+ case "urn:mas:admin":
+ return ;
+ default:
+ return ;
+ }
+};
+
+export const ScopeList: React.FC<{ scope: string }> = ({ scope }) => {
+ const scopes = scope.split(" ");
+ return (
+
+ {scopes.map((scope) => (
+
+ ))}
+
+ );
+};
+
+export const Data: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+ {children}
+);
+
+export const DataLabel: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+
+ {children}
+
+);
+
+export const DataValue: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+
+ {children}
+
+);
+
+export const DataList: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+
+);
+
+export const DataSection: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+
+);
+
+export const DataSectionHeader: React.FC<
+ React.PropsWithChildren<{ className?: string }>
+> = ({ children, className }) => (
+
+ {children}
+
+
+);
diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap
index 850626e72..c4ccb5f05 100644
--- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap
+++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap
@@ -3,7 +3,7 @@
exports[` > renders a compatability session details 1`] = `
-
- Session
-
-
+ Device details
+
+
+
Last Active
-
- Inactive for 90+ days
-
-
-
+ Inactive for 90+ days
+
+
+
+
Signed in
-
- Thu, 29 Jun 2023, 03:35
-
-
-
+ Thu, 29 Jun 2023, 03:35
+
+
+
+
Device ID
abcd1234
-
-
+
IP Address
-
- 1.2.3.4
-
-
-
-
+ 1.2.3.4
+
+
+
+
+
Scopes
> renders a compatability session details 1`] = `
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
-
- See your profile info and contact details
-
+ See your profile info and contact details
-
- View your existing messages and data
-
+ View your existing messages and data
> renders a compatability session details 1`] = `
fill-rule="evenodd"
/>
-
- Send new messages on your behalf
-
+ Send new messages on your behalf
-
-
-
-
+
+
+
+
> renders a compatability session details 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
- Sign out
+ Remove device
@@ -264,7 +272,7 @@ exports[` > renders a compatability session details 1`] = `
exports[` > renders a compatability session without an ssoLogin 1`] = `
-
- Session
-
-
+ Device details
+
+
+
Last Active
-
- Inactive for 90+ days
-
-
-
+ Inactive for 90+ days
+
+
+
+
Signed in
-
- Thu, 29 Jun 2023, 03:35
-
-
-
+ Thu, 29 Jun 2023, 03:35
+
+
+
+
Device ID
abcd1234
-
-
+
IP Address
-
- 1.2.3.4
-
-
-
-
+ 1.2.3.4
+
+
+
+
+
Scopes
> renders a compatability session without an ssoL
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
-
- See your profile info and contact details
-
+ See your profile info and contact details
-
- View your existing messages and data
-
+ View your existing messages and data
> renders a compatability session without an ssoL
fill-rule="evenodd"
/>
-
- Send new messages on your behalf
-
+ Send new messages on your behalf
-
-
+
+
+
+
+ Client info
+
+
+
+
+
+ Name
+
+
+ Unknown device
+
+
+
+
+ Uri
+
+
+
+
+
> renders a compatability session without an ssoL
xmlns="http://www.w3.org/2000/svg"
>
- Sign out
+ Remove device
@@ -479,7 +539,7 @@ exports[` > renders a compatability session without an ssoL
exports[` > renders a finished compatability session details 1`] = `
-
- Session
-
-
+ Device details
+
+
+
Last Active
-
- Inactive for 90+ days
-
-
-
+ Inactive for 90+ days
+
+
+
+
Signed in
-
- Thu, 29 Jun 2023, 03:35
-
-
-
+ Thu, 29 Jun 2023, 03:35
+
+
+
+
+ Finished
+
+
+
+ Sat, 29 Jul 2023, 03:35
+
+
+
+
+
Device ID
abcd1234
-
-
+
IP Address
-
- 1.2.3.4
-
-
-
-
- Finished
-
-
- Sat, 29 Jul 2023, 03:35
-
-
-
-
+ 1.2.3.4
+
+
+
+
+
Scopes
> renders a finished compatability session detail
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
-
- See your profile info and contact details
-
+ See your profile info and contact details
-
- View your existing messages and data
-
+ View your existing messages and data
> renders a finished compatability session detail
fill-rule="evenodd"
/>
-
- Send new messages on your behalf
-
+ Send new messages on your behalf
-
-
-
-
+
+
+
+
`;
diff --git a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap
index 1b4a32865..3535ea0de 100644
--- a/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap
+++ b/frontend/src/components/SessionDetail/__snapshots__/OAuth2SessionDetail.test.tsx.snap
@@ -3,7 +3,7 @@
exports[` > renders a finished session details 1`] = `
-
Device details
-
-
+
+
+
Last Active
-
- Inactive for 90+ days
-
-
-
+ Inactive for 90+ days
+
+
+
+
Signed in
-
- Thu, 29 Jun 2023, 03:35
-
-
-
+ Thu, 29 Jun 2023, 03:35
+
+
+
+
+ Finished
+
+
+
+ Sat, 29 Jul 2023, 03:35
+
+
+
+
+
Device ID
abcd1234
-
-
+
IP Address
-
- 1.2.3.4
-
-
-
-
- Finished
-
-
- Sat, 29 Jul 2023, 03:35
-
-
-
-
+ 1.2.3.4
+
+
+
+
+
Scopes
> renders a finished session details 1`] = `
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
-
- See your profile info and contact details
-
+ See your profile info and contact details
-
- View your existing messages and data
-
+ View your existing messages and data
> renders a finished session details 1`] = `
fill-rule="evenodd"
/>
-
- Send new messages on your behalf
-
+ Send new messages on your behalf
-
-
-
+
+
-
-
+ Element
+
+
+
Client ID
-
- test-client-id
-
-
-
-
-
+
+ https://element.io
+
+
+
+
+
`;
@@ -265,7 +284,7 @@ exports[` > renders a finished session details 1`] = `
exports[` > renders session details 1`] = `
-
Device details
-
-
+
+
+
Last Active
-
- Inactive for 90+ days
-
-
-
+ Inactive for 90+ days
+
+
+
+
Signed in
-
- Thu, 29 Jun 2023, 03:35
-
-
-
+ Thu, 29 Jun 2023, 03:35
+
+
+
+
Device ID
abcd1234
-
-
+
IP Address
-
- 1.2.3.4
-
-
-
-
+ 1.2.3.4
+
+
+
+
+
Scopes
> renders session details 1`] = `
d="M16.23 18.792a12.47 12.47 0 0 0-1.455-.455 11.6 11.6 0 0 0-5.55 0c-.487.12-.972.271-1.455.455a8.04 8.04 0 0 1-1.729-1.454c.89-.412 1.794-.729 2.709-.95A13.76 13.76 0 0 1 12 16c1.1 0 2.183.13 3.25.387a14.78 14.78 0 0 1 2.709.95 8.042 8.042 0 0 1-1.73 1.455Z"
/>
-
- See your profile info and contact details
-
+ See your profile info and contact details
-
- View your existing messages and data
-
+ View your existing messages and data
> renders session details 1`] = `
fill-rule="evenodd"
/>
-
- Send new messages on your behalf
-
+ Send new messages on your behalf
-
-
-
+
+
-
-
+ Element
+
+
+
Client ID
-
- test-client-id
-
-
-
-
-
+
+ https://element.io
+
+
+
+
+
> renders session details 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
- Sign out
+ Remove device
diff --git a/frontend/src/components/VisualList/VisualList.module.css b/frontend/src/components/VisualList/VisualList.module.css
deleted file mode 100644
index 7f60c94bf..000000000
--- a/frontend/src/components/VisualList/VisualList.module.css
+++ /dev/null
@@ -1,28 +0,0 @@
-/* Copyright 2024 New Vector Ltd.
-* Copyright 2024 The Matrix.org Foundation C.I.C.
-*
-* SPDX-License-Identifier: AGPL-3.0-only
-* Please see LICENSE in the repository root for full details.
- */
-
-.list {
- display: flex;
- flex-direction: column;
- gap: var(--cpd-space-scale);
- border-radius: var(--cpd-space-5x);
- overflow: hidden;
-}
-
-.item {
- background: var(--cpd-color-bg-action-secondary-hovered);
- padding: var(--cpd-space-3x) var(--cpd-space-5x);
- display: flex;
- align-items: center;
- gap: var(--cpd-space-3x);
-}
-
-.item svg {
- inline-size: var(--cpd-space-6x);
- block-size: var(--cpd-space-6x);
- flex-shrink: 0;
-}
diff --git a/frontend/src/components/VisualList/VisualList.tsx b/frontend/src/components/VisualList/VisualList.tsx
deleted file mode 100644
index 7eda8818b..000000000
--- a/frontend/src/components/VisualList/VisualList.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2024 New Vector Ltd.
-// Copyright 2024 The Matrix.org Foundation C.I.C.
-//
-// SPDX-License-Identifier: AGPL-3.0-only
-// Please see LICENSE in the repository root for full details.
-
-import { Text } from "@vector-im/compound-web";
-import type {
- FC,
- ForwardRefExoticComponent,
- ReactNode,
- RefAttributes,
- SVGProps,
-} from "react";
-
-import styles from "./VisualList.module.css";
-
-type Props = {
- children: ReactNode;
-};
-
-export const VisualListItem: FC<{
- Icon: ForwardRefExoticComponent<
- Omit, "ref" | "children"> &
- RefAttributes
- >;
- iconColor?: string;
- label: string;
-}> = ({ Icon, iconColor, label }) => {
- return (
-
-
- {label}
-
- );
-};
-
-export const VisualList: React.FC = ({ children }) => {
- return ;
-};
diff --git a/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap
index 8b80f55e2..0421e8bd3 100644
--- a/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap
+++ b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap
@@ -182,10 +182,10 @@ exports[` > renders an active session 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
- Sign out
+ Remove device
diff --git a/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap
index 14c5b3da6..1c27b6b89 100644
--- a/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap
+++ b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap
@@ -176,10 +176,10 @@ exports[` > renders an active session 1`] = `
xmlns="http://www.w3.org/2000/svg"
>
- Sign out
+ Remove device
diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts
index 9f144ba22..0254e2a1b 100644
--- a/frontend/src/gql/gql.ts
+++ b/frontend/src/gql/gql.ts
@@ -16,19 +16,22 @@ import * as types from './graphql';
*/
type Documents = {
"\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": typeof types.PasswordChange_SiteConfigFragmentDoc,
- "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": typeof types.BrowserSession_SessionFragmentDoc,
- "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": typeof types.EndBrowserSessionDocument,
+ "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": typeof types.BrowserSession_SessionFragmentDoc,
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": typeof types.OAuth2Client_DetailFragmentDoc,
- "\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 types.CompatSession_SessionFragmentDoc,
- "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": typeof types.EndCompatSessionDocument,
+ "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_SessionFragmentDoc,
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc,
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument,
- "\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 types.OAuth2Session_SessionFragmentDoc,
- "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument,
+ "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\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 types.OAuth2Session_SessionFragmentDoc,
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
- "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc,
- "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
- "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
+ "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc,
+ "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument,
+ "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.EndCompatSessionButton_SessionFragmentDoc,
+ "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": typeof types.EndCompatSessionDocument,
+ "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.EndOAuth2SessionButton_SessionFragmentDoc,
+ "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": typeof types.EndOAuth2SessionDocument,
+ "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": typeof types.BrowserSession_DetailFragmentDoc,
+ "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.CompatSession_DetailFragmentDoc,
+ "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": typeof types.OAuth2Session_DetailFragmentDoc,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": typeof types.UserEmail_EmailFragmentDoc,
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmail_SiteConfigFragmentDoc,
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": typeof types.RemoveEmailDocument,
@@ -39,12 +42,11 @@ type Documents = {
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": typeof types.UserEmailListDocument,
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
- "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
- "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": typeof types.SessionDetailDocument,
+ "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
- "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": typeof types.CurrentUserGreetingDocument,
+ "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": typeof types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": typeof types.OAuth2ClientDocument,
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.CurrentViewerDocument,
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.DeviceRedirectDocument,
@@ -59,22 +61,26 @@ type Documents = {
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc,
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": typeof types.PasswordRecoveryDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": typeof types.AllowCrossSigningResetDocument,
+ "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": typeof types.SessionDetailDocument,
};
const documents: Documents = {
"\n fragment PasswordChange_siteConfig on SiteConfig {\n passwordChangeAllowed\n }\n": types.PasswordChange_SiteConfigFragmentDoc,
- "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": types.BrowserSession_SessionFragmentDoc,
- "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": types.EndBrowserSessionDocument,
+ "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n": types.BrowserSession_SessionFragmentDoc,
"\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 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 }\n }\n }\n": types.EndCompatSessionDocument,
+ "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_SessionFragmentDoc,
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc,
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument,
- "\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 }\n }\n }\n": types.EndOAuth2SessionDocument,
+ "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\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 fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
- "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc,
- "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
- "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
+ "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc,
+ "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument,
+ "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.EndCompatSessionButton_SessionFragmentDoc,
+ "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n": types.EndCompatSessionDocument,
+ "\n fragment EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.EndOAuth2SessionButton_SessionFragmentDoc,
+ "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n": types.EndOAuth2SessionDocument,
+ "\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc,
+ "\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
+ "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n }\n": types.UserEmail_EmailFragmentDoc,
"\n fragment UserEmail_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmail_SiteConfigFragmentDoc,
"\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument,
@@ -85,12 +91,11 @@ const documents: Documents = {
"\n query UserEmailList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewer {\n __typename\n ... on User {\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n": types.UserEmailListDocument,
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
- "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
- "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument,
+ "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
- "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument,
+ "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerDocument,
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectDocument,
@@ -105,6 +110,7 @@ const documents: Documents = {
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": types.RecoverPassword_SiteConfigFragmentDoc,
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": types.PasswordRecoveryDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
+ "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailDocument,
};
/**
@@ -114,11 +120,7 @@ export function graphql(source: "\n fragment PasswordChange_siteConfig on SiteC
/**
* 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 BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n"): typeof import('./graphql').BrowserSession_SessionFragmentDoc;
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n"): typeof import('./graphql').EndBrowserSessionDocument;
+export function graphql(source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n deviceType\n name\n os\n model\n }\n lastActiveAt\n }\n"): typeof import('./graphql').BrowserSession_SessionFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -126,11 +128,7 @@ export function graphql(source: "\n fragment OAuth2Client_detail on Oauth2Clien
/**
* 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 name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc;
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndCompatSessionDocument;
+export function graphql(source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n ...EndCompatSessionButton_session\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_SessionFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -142,11 +140,7 @@ export function graphql(source: "\n query Footer {\n siteConfig {\n id\
/**
* 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 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 import('./graphql').OAuth2Session_SessionFragmentDoc;
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionDocument;
+export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\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 import('./graphql').OAuth2Session_SessionFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -154,15 +148,39 @@ export function graphql(source: "\n fragment PasswordCreationDoubleInput_siteCo
/**
* 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 BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"): typeof import('./graphql').BrowserSession_DetailFragmentDoc;
+export function graphql(source: "\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n"): typeof import('./graphql').EndBrowserSessionButton_SessionFragmentDoc;
/**
* 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_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc;
+export function graphql(source: "\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndBrowserSessionDocument;
/**
* 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_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc;
+export function graphql(source: "\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').EndCompatSessionButton_SessionFragmentDoc;
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n }\n }\n }\n"): typeof import('./graphql').EndCompatSessionDocument;
+/**
+ * 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 EndOAuth2SessionButton_session on Oauth2Session {\n id\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionButton_SessionFragmentDoc;
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n }\n }\n }\n"): typeof import('./graphql').EndOAuth2SessionDocument;
+/**
+ * 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 BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n ...EndBrowserSessionButton_session\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n"): typeof import('./graphql').BrowserSession_DetailFragmentDoc;
+/**
+ * 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_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndCompatSessionButton_session\n\n userAgent {\n name\n os\n model\n }\n\n ssoLogin {\n id\n redirectUri\n }\n }\n"): typeof import('./graphql').CompatSession_DetailFragmentDoc;
+/**
+ * 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_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n }\n\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_DetailFragmentDoc;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -206,11 +224,7 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "\n query UserProfile {\n viewer {\n __typename\n ... on User {\n emails(first: 0) {\n totalCount\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
-/**
- * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
- */
-export function graphql(source: "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n"): typeof import('./graphql').SessionDetailDocument;
+export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -226,7 +240,7 @@ export function graphql(source: "\n query AppSessionsList(\n $before: String
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n ...UserGreeting_user\n }\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
+export function graphql(source: "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -283,6 +297,10 @@ export function graphql(source: "\n query PasswordRecovery($ticket: String!) {\
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n"): typeof import('./graphql').AllowCrossSigningResetDocument;
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "\n query SessionDetail($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n"): typeof import('./graphql').SessionDetailDocument;
export function graphql(source: string) {
diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts
index cf32c2302..6564a67da 100644
--- a/frontend/src/gql/graphql.ts
+++ b/frontend/src/gql/graphql.ts
@@ -1565,28 +1565,17 @@ export type ViewerSession = Anonymous | BrowserSession | Oauth2Session;
export type PasswordChange_SiteConfigFragment = { __typename?: 'SiteConfig', passwordChangeAllowed: boolean } & { ' $fragmentName'?: 'PasswordChange_SiteConfigFragment' };
-export type BrowserSession_SessionFragment = { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', raw: string, name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null } & { ' $fragmentName'?: 'BrowserSession_SessionFragment' };
-
-export type EndBrowserSessionMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type EndBrowserSessionMutation = { __typename?: 'Mutation', endBrowserSession: { __typename?: 'EndBrowserSessionPayload', status: EndBrowserSessionStatus, browserSession?: (
- { __typename?: 'BrowserSession', id: string }
- & { ' $fragmentRefs'?: { 'BrowserSession_SessionFragment': BrowserSession_SessionFragment } }
- ) | null } };
+export type BrowserSession_SessionFragment = (
+ { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', deviceType: DeviceType, name?: string | null, os?: string | null, model?: string | null } | null }
+ & { ' $fragmentRefs'?: { 'EndBrowserSessionButton_SessionFragment': EndBrowserSessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'BrowserSession_SessionFragment' };
export type OAuth2Client_DetailFragment = { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null, tosUri?: string | null, policyUri?: string | null, redirectUris: Array } & { ' $fragmentName'?: 'OAuth2Client_DetailFragment' };
-export type CompatSession_SessionFragment = { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'CompatSession_SessionFragment' };
-
-export type EndCompatSessionMutationVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string } | null } };
+export type CompatSession_SessionFragment = (
+ { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
+ & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'CompatSession_SessionFragment' };
export type Footer_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, imprint?: string | null, tosUri?: string | null, policyUri?: string | null } & { ' $fragmentName'?: 'Footer_SiteConfigFragment' };
@@ -1598,7 +1587,32 @@ export type FooterQuery = { __typename?: 'Query', siteConfig: (
& { ' $fragmentRefs'?: { 'Footer_SiteConfigFragment': Footer_SiteConfigFragment } }
) };
-export type OAuth2Session_SessionFragment = { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' };
+export type OAuth2Session_SessionFragment = (
+ { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } }
+ & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' };
+
+export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
+
+export type EndBrowserSessionButton_SessionFragment = { __typename?: 'BrowserSession', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null } & { ' $fragmentName'?: 'EndBrowserSessionButton_SessionFragment' };
+
+export type EndBrowserSessionMutationVariables = Exact<{
+ id: Scalars['ID']['input'];
+}>;
+
+
+export type EndBrowserSessionMutation = { __typename?: 'Mutation', endBrowserSession: { __typename?: 'EndBrowserSessionPayload', status: EndBrowserSessionStatus, browserSession?: { __typename?: 'BrowserSession', id: string } | null } };
+
+export type EndCompatSessionButton_SessionFragment = { __typename?: 'CompatSession', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null, deviceType: DeviceType } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'EndCompatSessionButton_SessionFragment' };
+
+export type EndCompatSessionMutationVariables = Exact<{
+ id: Scalars['ID']['input'];
+}>;
+
+
+export type EndCompatSessionMutation = { __typename?: 'Mutation', endCompatSession: { __typename?: 'EndCompatSessionPayload', status: EndCompatSessionStatus, compatSession?: { __typename?: 'CompatSession', id: string } | null } };
+
+export type EndOAuth2SessionButton_SessionFragment = { __typename?: 'Oauth2Session', id: string, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null, deviceType: DeviceType } | null, client: { __typename?: 'Oauth2Client', clientId: string, clientName?: string | null, applicationType?: Oauth2ApplicationType | null, logoUri?: string | null } } & { ' $fragmentName'?: 'EndOAuth2SessionButton_SessionFragment' };
export type EndOAuth2SessionMutationVariables = Exact<{
id: Scalars['ID']['input'];
@@ -1607,13 +1621,20 @@ export type EndOAuth2SessionMutationVariables = Exact<{
export type EndOAuth2SessionMutation = { __typename?: 'Mutation', endOauth2Session: { __typename?: 'EndOAuth2SessionPayload', status: EndOAuth2SessionStatus, oauth2Session?: { __typename?: 'Oauth2Session', id: string } | null } };
-export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
+export type BrowserSession_DetailFragment = (
+ { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null, user: { __typename?: 'User', id: string, username: string } }
+ & { ' $fragmentRefs'?: { 'EndBrowserSessionButton_SessionFragment': EndBrowserSessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'BrowserSession_DetailFragment' };
-export type BrowserSession_DetailFragment = { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null, user: { __typename?: 'User', id: string, username: string } } & { ' $fragmentName'?: 'BrowserSession_DetailFragment' };
+export type CompatSession_DetailFragment = (
+ { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null }
+ & { ' $fragmentRefs'?: { 'EndCompatSessionButton_SessionFragment': EndCompatSessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'CompatSession_DetailFragment' };
-export type CompatSession_DetailFragment = { __typename?: 'CompatSession', id: string, createdAt: string, deviceId?: string | null, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'CompatSession_DetailFragment' };
-
-export type OAuth2Session_DetailFragment = { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } } & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' };
+export type OAuth2Session_DetailFragment = (
+ { __typename?: 'Oauth2Session', id: string, scope: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, client: { __typename?: 'Oauth2Client', id: string, clientId: string, clientName?: string | null, clientUri?: string | null, logoUri?: string | null } }
+ & { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
+) & { ' $fragmentName'?: 'OAuth2Session_DetailFragment' };
export type UserEmail_EmailFragment = { __typename?: 'UserEmail', id: string, email: string } & { ' $fragmentName'?: 'UserEmail_EmailFragment' };
@@ -1666,27 +1687,11 @@ export type BrowserSessionsOverview_UserFragment = { __typename?: 'User', id: st
export type UserProfileQueryVariables = Exact<{ [key: string]: never; }>;
-export type UserProfileQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | { __typename: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } }, siteConfig: (
+export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: { __typename?: 'User', emails: { __typename?: 'UserEmailConnection', totalCount: number } } } | { __typename: 'Oauth2Session' }, siteConfig: (
{ __typename?: 'SiteConfig', emailChangeAllowed: boolean, passwordLoginEnabled: boolean }
& { ' $fragmentRefs'?: { 'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'UserEmail_SiteConfigFragment': UserEmail_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment } }
) };
-export type SessionDetailQueryVariables = Exact<{
- id: Scalars['ID']['input'];
-}>;
-
-
-export type SessionDetailQuery = { __typename?: 'Query', viewerSession: { __typename?: 'Anonymous', id: string } | { __typename?: 'BrowserSession', id: string } | { __typename?: 'Oauth2Session', id: string }, node?: { __typename: 'Anonymous', id: string } | { __typename: 'Authentication', id: string } | (
- { __typename: 'BrowserSession', id: string }
- & { ' $fragmentRefs'?: { 'BrowserSession_DetailFragment': BrowserSession_DetailFragment } }
- ) | (
- { __typename: 'CompatSession', id: string }
- & { ' $fragmentRefs'?: { 'CompatSession_DetailFragment': CompatSession_DetailFragment } }
- ) | { __typename: 'CompatSsoLogin', id: string } | { __typename: 'Oauth2Client', id: string } | (
- { __typename: 'Oauth2Session', id: string }
- & { ' $fragmentRefs'?: { 'OAuth2Session_DetailFragment': OAuth2Session_DetailFragment } }
- ) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserEmailAuthentication', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null };
-
export type BrowserSessionListQueryVariables = Exact<{
first?: InputMaybe;
after?: InputMaybe;
@@ -1729,10 +1734,10 @@ export type AppSessionsListQuery = { __typename?: 'Query', viewer: { __typename:
export type CurrentUserGreetingQueryVariables = Exact<{ [key: string]: never; }>;
-export type CurrentUserGreetingQuery = { __typename?: 'Query', viewerSession: { __typename: 'Anonymous' } | { __typename: 'BrowserSession', id: string, user: (
- { __typename?: 'User' }
- & { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } }
- ) } | { __typename: 'Oauth2Session' }, siteConfig: (
+export type CurrentUserGreetingQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous' } | (
+ { __typename: 'User' }
+ & { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } }
+ ), siteConfig: (
{ __typename?: 'SiteConfig' }
& { ' $fragmentRefs'?: { 'UserGreeting_SiteConfigFragment': UserGreeting_SiteConfigFragment } }
) };
@@ -1842,6 +1847,22 @@ export type AllowCrossSigningResetMutationVariables = Exact<{
export type AllowCrossSigningResetMutation = { __typename?: 'Mutation', allowUserCrossSigningReset: { __typename?: 'AllowUserCrossSigningResetPayload', user?: { __typename?: 'User', id: string } | null } };
+export type SessionDetailQueryVariables = Exact<{
+ id: Scalars['ID']['input'];
+}>;
+
+
+export type SessionDetailQuery = { __typename?: 'Query', viewerSession: { __typename?: 'Anonymous', id: string } | { __typename?: 'BrowserSession', id: string } | { __typename?: 'Oauth2Session', id: string }, node?: { __typename: 'Anonymous', id: string } | { __typename: 'Authentication', id: string } | (
+ { __typename: 'BrowserSession', id: string }
+ & { ' $fragmentRefs'?: { 'BrowserSession_DetailFragment': BrowserSession_DetailFragment } }
+ ) | (
+ { __typename: 'CompatSession', id: string }
+ & { ' $fragmentRefs'?: { 'CompatSession_DetailFragment': CompatSession_DetailFragment } }
+ ) | { __typename: 'CompatSsoLogin', id: string } | { __typename: 'Oauth2Client', id: string } | (
+ { __typename: 'Oauth2Session', id: string }
+ & { ' $fragmentRefs'?: { 'OAuth2Session_DetailFragment': OAuth2Session_DetailFragment } }
+ ) | { __typename: 'SiteConfig', id: string } | { __typename: 'UpstreamOAuth2Link', id: string } | { __typename: 'UpstreamOAuth2Provider', id: string } | { __typename: 'User', id: string } | { __typename: 'UserEmail', id: string } | { __typename: 'UserEmailAuthentication', id: string } | { __typename: 'UserRecoveryTicket', id: string } | null };
+
export class TypedDocumentString
extends String
implements DocumentTypeDecoration
@@ -1861,26 +1882,40 @@ export const PasswordChange_SiteConfigFragmentDoc = new TypedDocumentString(`
passwordChangeAllowed
}
`, {"fragmentName":"PasswordChange_siteConfig"}) as unknown as TypedDocumentString;
-export const BrowserSession_SessionFragmentDoc = new TypedDocumentString(`
- fragment BrowserSession_session on BrowserSession {
+export const EndBrowserSessionButton_SessionFragmentDoc = new TypedDocumentString(`
+ fragment EndBrowserSessionButton_session on BrowserSession {
id
- createdAt
- finishedAt
userAgent {
- raw
name
os
model
deviceType
}
- lastActiveIp
- lastActiveAt
- lastAuthentication {
- id
- createdAt
- }
}
- `, {"fragmentName":"BrowserSession_session"}) as unknown as TypedDocumentString;
+ `, {"fragmentName":"EndBrowserSessionButton_session"}) as unknown as TypedDocumentString;
+export const BrowserSession_SessionFragmentDoc = new TypedDocumentString(`
+ fragment BrowserSession_session on BrowserSession {
+ id
+ createdAt
+ finishedAt
+ ...EndBrowserSessionButton_session
+ userAgent {
+ deviceType
+ name
+ os
+ model
+ }
+ lastActiveAt
+}
+ fragment EndBrowserSessionButton_session on BrowserSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+}`, {"fragmentName":"BrowserSession_session"}) as unknown as TypedDocumentString;
export const OAuth2Client_DetailFragmentDoc = new TypedDocumentString(`
fragment OAuth2Client_detail on Oauth2Client {
id
@@ -1893,14 +1928,9 @@ export const OAuth2Client_DetailFragmentDoc = new TypedDocumentString(`
redirectUris
}
`, {"fragmentName":"OAuth2Client_detail"}) as unknown as TypedDocumentString;
-export const CompatSession_SessionFragmentDoc = new TypedDocumentString(`
- fragment CompatSession_session on CompatSession {
+export const EndCompatSessionButton_SessionFragmentDoc = new TypedDocumentString(`
+ fragment EndCompatSessionButton_session on CompatSession {
id
- createdAt
- deviceId
- finishedAt
- lastActiveIp
- lastActiveAt
userAgent {
name
os
@@ -1912,7 +1942,40 @@ export const CompatSession_SessionFragmentDoc = new TypedDocumentString(`
redirectUri
}
}
- `, {"fragmentName":"CompatSession_session"}) as unknown as TypedDocumentString;
+ `, {"fragmentName":"EndCompatSessionButton_session"}) as unknown as TypedDocumentString;
+export const CompatSession_SessionFragmentDoc = new TypedDocumentString(`
+ fragment CompatSession_session on CompatSession {
+ id
+ createdAt
+ deviceId
+ finishedAt
+ lastActiveIp
+ lastActiveAt
+ ...EndCompatSessionButton_session
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}
+ fragment EndCompatSessionButton_session on CompatSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}`, {"fragmentName":"CompatSession_session"}) as unknown as TypedDocumentString;
export const Footer_SiteConfigFragmentDoc = new TypedDocumentString(`
fragment Footer_siteConfig on SiteConfig {
id
@@ -1921,6 +1984,23 @@ export const Footer_SiteConfigFragmentDoc = new TypedDocumentString(`
policyUri
}
`, {"fragmentName":"Footer_siteConfig"}) as unknown as TypedDocumentString;
+export const EndOAuth2SessionButton_SessionFragmentDoc = new TypedDocumentString(`
+ fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
+}
+ `, {"fragmentName":"EndOAuth2SessionButton_session"}) as unknown as TypedDocumentString;
export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(`
fragment OAuth2Session_session on Oauth2Session {
id
@@ -1929,6 +2009,7 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(`
finishedAt
lastActiveIp
lastActiveAt
+ ...EndOAuth2SessionButton_session
userAgent {
name
model
@@ -1943,12 +2024,27 @@ export const OAuth2Session_SessionFragmentDoc = new TypedDocumentString(`
logoUri
}
}
- `, {"fragmentName":"OAuth2Session_session"}) as unknown as TypedDocumentString;
+ fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
+}`, {"fragmentName":"OAuth2Session_session"}) as unknown as TypedDocumentString;
export const BrowserSession_DetailFragmentDoc = new TypedDocumentString(`
fragment BrowserSession_detail on BrowserSession {
id
createdAt
finishedAt
+ ...EndBrowserSessionButton_session
userAgent {
name
model
@@ -1965,7 +2061,15 @@ export const BrowserSession_DetailFragmentDoc = new TypedDocumentString(`
username
}
}
- `, {"fragmentName":"BrowserSession_detail"}) as unknown as TypedDocumentString;
+ fragment EndBrowserSessionButton_session on BrowserSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+}`, {"fragmentName":"BrowserSession_detail"}) as unknown as TypedDocumentString;
export const CompatSession_DetailFragmentDoc = new TypedDocumentString(`
fragment CompatSession_detail on CompatSession {
id
@@ -1974,6 +2078,7 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(`
finishedAt
lastActiveIp
lastActiveAt
+ ...EndCompatSessionButton_session
userAgent {
name
os
@@ -1984,7 +2089,19 @@ export const CompatSession_DetailFragmentDoc = new TypedDocumentString(`
redirectUri
}
}
- `, {"fragmentName":"CompatSession_detail"}) as unknown as TypedDocumentString;
+ fragment EndCompatSessionButton_session on CompatSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}`, {"fragmentName":"CompatSession_detail"}) as unknown as TypedDocumentString;
export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(`
fragment OAuth2Session_detail on Oauth2Session {
id
@@ -1993,6 +2110,12 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(`
finishedAt
lastActiveIp
lastActiveAt
+ ...EndOAuth2SessionButton_session
+ userAgent {
+ name
+ model
+ os
+ }
client {
id
clientId
@@ -2001,7 +2124,21 @@ export const OAuth2Session_DetailFragmentDoc = new TypedDocumentString(`
logoUri
}
}
- `, {"fragmentName":"OAuth2Session_detail"}) as unknown as TypedDocumentString;
+ fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
+}`, {"fragmentName":"OAuth2Session_detail"}) as unknown as TypedDocumentString;
export const UserEmail_EmailFragmentDoc = new TypedDocumentString(`
fragment UserEmail_email on UserEmail {
id
@@ -2060,44 +2197,6 @@ export const RecoverPassword_SiteConfigFragmentDoc = new TypedDocumentString(`
id
minimumPasswordComplexity
}`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString;
-export const EndBrowserSessionDocument = new TypedDocumentString(`
- mutation EndBrowserSession($id: ID!) {
- endBrowserSession(input: {browserSessionId: $id}) {
- status
- browserSession {
- id
- ...BrowserSession_session
- }
- }
-}
- fragment BrowserSession_session on BrowserSession {
- id
- createdAt
- finishedAt
- userAgent {
- raw
- name
- os
- model
- deviceType
- }
- lastActiveIp
- lastActiveAt
- lastAuthentication {
- id
- createdAt
- }
-}`) as unknown as TypedDocumentString;
-export const EndCompatSessionDocument = new TypedDocumentString(`
- mutation EndCompatSession($id: ID!) {
- endCompatSession(input: {compatSessionId: $id}) {
- status
- compatSession {
- id
- }
- }
-}
- `) as unknown as TypedDocumentString;
export const FooterDocument = new TypedDocumentString(`
query Footer {
siteConfig {
@@ -2111,6 +2210,26 @@ export const FooterDocument = new TypedDocumentString(`
tosUri
policyUri
}`) as unknown as TypedDocumentString;
+export const EndBrowserSessionDocument = new TypedDocumentString(`
+ mutation EndBrowserSession($id: ID!) {
+ endBrowserSession(input: {browserSessionId: $id}) {
+ status
+ browserSession {
+ id
+ }
+ }
+}
+ `) as unknown as TypedDocumentString;
+export const EndCompatSessionDocument = new TypedDocumentString(`
+ mutation EndCompatSession($id: ID!) {
+ endCompatSession(input: {compatSessionId: $id}) {
+ status
+ compatSession {
+ id
+ }
+ }
+}
+ `) as unknown as TypedDocumentString;
export const EndOAuth2SessionDocument = new TypedDocumentString(`
mutation EndOAuth2Session($id: ID!) {
endOauth2Session(input: {oauth2SessionId: $id}) {
@@ -2178,11 +2297,14 @@ export const UserEmailListDocument = new TypedDocumentString(`
}`) as unknown as TypedDocumentString;
export const UserProfileDocument = new TypedDocumentString(`
query UserProfile {
- viewer {
+ viewerSession {
__typename
- ... on User {
- emails(first: 0) {
- totalCount
+ ... on BrowserSession {
+ id
+ user {
+ emails(first: 0) {
+ totalCount
+ }
}
}
}
@@ -2203,73 +2325,6 @@ fragment UserEmail_siteConfig on SiteConfig {
fragment UserEmailList_siteConfig on SiteConfig {
emailChangeAllowed
}`) as unknown as TypedDocumentString;
-export const SessionDetailDocument = new TypedDocumentString(`
- query SessionDetail($id: ID!) {
- viewerSession {
- ... on Node {
- id
- }
- }
- node(id: $id) {
- __typename
- id
- ...CompatSession_detail
- ...OAuth2Session_detail
- ...BrowserSession_detail
- }
-}
- fragment BrowserSession_detail on BrowserSession {
- id
- createdAt
- finishedAt
- userAgent {
- name
- model
- os
- }
- lastActiveIp
- lastActiveAt
- lastAuthentication {
- id
- createdAt
- }
- user {
- id
- username
- }
-}
-fragment CompatSession_detail on CompatSession {
- id
- createdAt
- deviceId
- finishedAt
- lastActiveIp
- lastActiveAt
- userAgent {
- name
- os
- model
- }
- ssoLogin {
- id
- redirectUri
- }
-}
-fragment OAuth2Session_detail on Oauth2Session {
- id
- scope
- createdAt
- finishedAt
- lastActiveIp
- lastActiveAt
- client {
- id
- clientId
- clientName
- clientUri
- logoUri
- }
-}`) as unknown as TypedDocumentString;
export const BrowserSessionListDocument = new TypedDocumentString(`
query BrowserSessionList($first: Int, $after: String, $last: Int, $before: String, $lastActive: DateFilter) {
viewerSession {
@@ -2309,19 +2364,23 @@ export const BrowserSessionListDocument = new TypedDocumentString(`
id
createdAt
finishedAt
+ ...EndBrowserSessionButton_session
+ userAgent {
+ deviceType
+ name
+ os
+ model
+ }
+ lastActiveAt
+}
+fragment EndBrowserSessionButton_session on BrowserSession {
+ id
userAgent {
- raw
name
os
model
deviceType
}
- lastActiveIp
- lastActiveAt
- lastAuthentication {
- id
- createdAt
- }
}`) as unknown as TypedDocumentString;
export const SessionsOverviewDocument = new TypedDocumentString(`
query SessionsOverview {
@@ -2379,6 +2438,7 @@ export const AppSessionsListDocument = new TypedDocumentString(`
finishedAt
lastActiveIp
lastActiveAt
+ ...EndCompatSessionButton_session
userAgent {
name
os
@@ -2397,6 +2457,7 @@ fragment OAuth2Session_session on Oauth2Session {
finishedAt
lastActiveIp
lastActiveAt
+ ...EndOAuth2SessionButton_session
userAgent {
name
model
@@ -2410,16 +2471,41 @@ fragment OAuth2Session_session on Oauth2Session {
applicationType
logoUri
}
+}
+fragment EndCompatSessionButton_session on CompatSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}
+fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
}`) as unknown as TypedDocumentString;
export const CurrentUserGreetingDocument = new TypedDocumentString(`
query CurrentUserGreeting {
- viewerSession {
+ viewer {
__typename
- ... on BrowserSession {
- id
- user {
- ...UserGreeting_user
- }
+ ... on User {
+ ...UserGreeting_user
}
}
siteConfig {
@@ -2565,6 +2651,139 @@ export const AllowCrossSigningResetDocument = new TypedDocumentString(`
}
}
`) as unknown as TypedDocumentString;
+export const SessionDetailDocument = new TypedDocumentString(`
+ query SessionDetail($id: ID!) {
+ viewerSession {
+ ... on Node {
+ id
+ }
+ }
+ node(id: $id) {
+ __typename
+ id
+ ...CompatSession_detail
+ ...OAuth2Session_detail
+ ...BrowserSession_detail
+ }
+}
+ fragment EndBrowserSessionButton_session on BrowserSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+}
+fragment EndCompatSessionButton_session on CompatSession {
+ id
+ userAgent {
+ name
+ os
+ model
+ deviceType
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}
+fragment EndOAuth2SessionButton_session on Oauth2Session {
+ id
+ userAgent {
+ name
+ model
+ os
+ deviceType
+ }
+ client {
+ clientId
+ clientName
+ applicationType
+ logoUri
+ }
+}
+fragment BrowserSession_detail on BrowserSession {
+ id
+ createdAt
+ finishedAt
+ ...EndBrowserSessionButton_session
+ userAgent {
+ name
+ model
+ os
+ }
+ lastActiveIp
+ lastActiveAt
+ lastAuthentication {
+ id
+ createdAt
+ }
+ user {
+ id
+ username
+ }
+}
+fragment CompatSession_detail on CompatSession {
+ id
+ createdAt
+ deviceId
+ finishedAt
+ lastActiveIp
+ lastActiveAt
+ ...EndCompatSessionButton_session
+ userAgent {
+ name
+ os
+ model
+ }
+ ssoLogin {
+ id
+ redirectUri
+ }
+}
+fragment OAuth2Session_detail on Oauth2Session {
+ id
+ scope
+ createdAt
+ finishedAt
+ lastActiveIp
+ lastActiveAt
+ ...EndOAuth2SessionButton_session
+ userAgent {
+ name
+ model
+ os
+ }
+ client {
+ id
+ clientId
+ clientName
+ clientUri
+ logoUri
+ }
+}`) as unknown as TypedDocumentString;
+
+/**
+ * @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
+ * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
+ * @see https://mswjs.io/docs/basics/response-resolver
+ * @example
+ * mockFooterQuery(
+ * ({ query, variables }) => {
+ * return HttpResponse.json({
+ * data: { siteConfig }
+ * })
+ * },
+ * requestOptions
+ * )
+ */
+export const mockFooterQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) =>
+ graphql.query(
+ 'Footer',
+ resolver,
+ options
+ )
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
@@ -2610,27 +2829,6 @@ export const mockEndCompatSessionMutation = (resolver: GraphQLResponseResolver {
- * return HttpResponse.json({
- * data: { siteConfig }
- * })
- * },
- * requestOptions
- * )
- */
-export const mockFooterQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) =>
- graphql.query(
- 'Footer',
- resolver,
- options
- )
-
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
@@ -2749,7 +2947,7 @@ export const mockUserEmailListQuery = (resolver: GraphQLResponseResolver {
* return HttpResponse.json({
- * data: { viewer, siteConfig }
+ * data: { viewerSession, siteConfig }
* })
* },
* requestOptions
@@ -2762,28 +2960,6 @@ export const mockUserProfileQuery = (resolver: GraphQLResponseResolver {
- * const { id } = variables;
- * return HttpResponse.json({
- * data: { viewerSession, node }
- * })
- * },
- * requestOptions
- * )
- */
-export const mockSessionDetailQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) =>
- graphql.query(
- 'SessionDetail',
- resolver,
- options
- )
-
/**
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
@@ -2857,7 +3033,7 @@ export const mockAppSessionsListQuery = (resolver: GraphQLResponseResolver {
* return HttpResponse.json({
- * data: { viewerSession, siteConfig }
+ * data: { viewer, siteConfig }
* })
* },
* requestOptions
@@ -3131,3 +3307,25 @@ export const mockAllowCrossSigningResetMutation = (resolver: GraphQLResponseReso
resolver,
options
)
+
+/**
+ * @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
+ * @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
+ * @see https://mswjs.io/docs/basics/response-resolver
+ * @example
+ * mockSessionDetailQuery(
+ * ({ query, variables }) => {
+ * const { id } = variables;
+ * return HttpResponse.json({
+ * data: { viewerSession, node }
+ * })
+ * },
+ * requestOptions
+ * )
+ */
+export const mockSessionDetailQuery = (resolver: GraphQLResponseResolver, options?: RequestHandlerOptions) =>
+ graphql.query(
+ 'SessionDetail',
+ resolver,
+ options
+ )
diff --git a/frontend/src/routeTree.gen.ts b/frontend/src/routeTree.gen.ts
index 2c412c68c..d36635c93 100644
--- a/frontend/src/routeTree.gen.ts
+++ b/frontend/src/routeTree.gen.ts
@@ -17,6 +17,7 @@ import { Route as ResetCrossSigningImport } from './routes/reset-cross-signing'
import { Route as AccountImport } from './routes/_account'
import { Route as ResetCrossSigningIndexImport } from './routes/reset-cross-signing.index'
import { Route as AccountIndexImport } from './routes/_account.index'
+import { Route as SessionsIdImport } from './routes/sessions.$id'
import { Route as ResetCrossSigningSuccessImport } from './routes/reset-cross-signing.success'
import { Route as ResetCrossSigningCancelledImport } from './routes/reset-cross-signing.cancelled'
import { Route as DevicesSplatImport } from './routes/devices.$'
@@ -27,7 +28,6 @@ import { Route as AccountSessionsIndexImport } from './routes/_account.sessions.
import { Route as EmailsIdVerifyImport } from './routes/emails.$id.verify'
import { Route as EmailsIdInUseImport } from './routes/emails.$id.in-use'
import { Route as AccountSessionsBrowsersImport } from './routes/_account.sessions.browsers'
-import { Route as AccountSessionsIdImport } from './routes/_account.sessions.$id'
// Create Virtual Routes
@@ -62,6 +62,12 @@ const AccountIndexRoute = AccountIndexImport.update({
import('./routes/_account.index.lazy').then((d) => d.Route),
)
+const SessionsIdRoute = SessionsIdImport.update({
+ id: '/sessions/$id',
+ path: '/sessions/$id',
+ getParentRoute: () => rootRoute,
+} as any).lazy(() => import('./routes/sessions.$id.lazy').then((d) => d.Route))
+
const ResetCrossSigningSuccessRoute = ResetCrossSigningSuccessImport.update({
id: '/success',
path: '/success',
@@ -142,14 +148,6 @@ const AccountSessionsBrowsersRoute = AccountSessionsBrowsersImport.update({
import('./routes/_account.sessions.browsers.lazy').then((d) => d.Route),
)
-const AccountSessionsIdRoute = AccountSessionsIdImport.update({
- id: '/sessions/$id',
- path: '/sessions/$id',
- getParentRoute: () => AccountRoute,
-} as any).lazy(() =>
- import('./routes/_account.sessions.$id.lazy').then((d) => d.Route),
-)
-
// Populate the FileRoutesByPath interface
declare module '@tanstack/react-router' {
@@ -196,6 +194,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ResetCrossSigningSuccessImport
parentRoute: typeof ResetCrossSigningImport
}
+ '/sessions/$id': {
+ id: '/sessions/$id'
+ path: '/sessions/$id'
+ fullPath: '/sessions/$id'
+ preLoaderRoute: typeof SessionsIdImport
+ parentRoute: typeof rootRoute
+ }
'/_account/': {
id: '/_account/'
path: '/'
@@ -210,13 +215,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ResetCrossSigningIndexImport
parentRoute: typeof ResetCrossSigningImport
}
- '/_account/sessions/$id': {
- id: '/_account/sessions/$id'
- path: '/sessions/$id'
- fullPath: '/sessions/$id'
- preLoaderRoute: typeof AccountSessionsIdImport
- parentRoute: typeof AccountImport
- }
'/_account/sessions/browsers': {
id: '/_account/sessions/browsers'
path: '/sessions/browsers'
@@ -273,14 +271,12 @@ declare module '@tanstack/react-router' {
interface AccountRouteChildren {
AccountIndexRoute: typeof AccountIndexRoute
- AccountSessionsIdRoute: typeof AccountSessionsIdRoute
AccountSessionsBrowsersRoute: typeof AccountSessionsBrowsersRoute
AccountSessionsIndexRoute: typeof AccountSessionsIndexRoute
}
const AccountRouteChildren: AccountRouteChildren = {
AccountIndexRoute: AccountIndexRoute,
- AccountSessionsIdRoute: AccountSessionsIdRoute,
AccountSessionsBrowsersRoute: AccountSessionsBrowsersRoute,
AccountSessionsIndexRoute: AccountSessionsIndexRoute,
}
@@ -310,9 +306,9 @@ export interface FileRoutesByFullPath {
'/devices/$': typeof DevicesSplatRoute
'/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute
'/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute
+ '/sessions/$id': typeof SessionsIdRoute
'/': typeof AccountIndexRoute
'/reset-cross-signing/': typeof ResetCrossSigningIndexRoute
- '/sessions/$id': typeof AccountSessionsIdRoute
'/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/in-use': typeof EmailsIdInUseRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
@@ -327,9 +323,9 @@ export interface FileRoutesByTo {
'/devices/$': typeof DevicesSplatRoute
'/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute
'/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute
+ '/sessions/$id': typeof SessionsIdRoute
'/': typeof AccountIndexRoute
'/reset-cross-signing': typeof ResetCrossSigningIndexRoute
- '/sessions/$id': typeof AccountSessionsIdRoute
'/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/in-use': typeof EmailsIdInUseRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
@@ -347,9 +343,9 @@ export interface FileRoutesById {
'/devices/$': typeof DevicesSplatRoute
'/reset-cross-signing/cancelled': typeof ResetCrossSigningCancelledRoute
'/reset-cross-signing/success': typeof ResetCrossSigningSuccessRoute
+ '/sessions/$id': typeof SessionsIdRoute
'/_account/': typeof AccountIndexRoute
'/reset-cross-signing/': typeof ResetCrossSigningIndexRoute
- '/_account/sessions/$id': typeof AccountSessionsIdRoute
'/_account/sessions/browsers': typeof AccountSessionsBrowsersRoute
'/emails/$id/in-use': typeof EmailsIdInUseRoute
'/emails/$id/verify': typeof EmailsIdVerifyRoute
@@ -368,9 +364,9 @@ export interface FileRouteTypes {
| '/devices/$'
| '/reset-cross-signing/cancelled'
| '/reset-cross-signing/success'
+ | '/sessions/$id'
| '/'
| '/reset-cross-signing/'
- | '/sessions/$id'
| '/sessions/browsers'
| '/emails/$id/in-use'
| '/emails/$id/verify'
@@ -384,9 +380,9 @@ export interface FileRouteTypes {
| '/devices/$'
| '/reset-cross-signing/cancelled'
| '/reset-cross-signing/success'
+ | '/sessions/$id'
| '/'
| '/reset-cross-signing'
- | '/sessions/$id'
| '/sessions/browsers'
| '/emails/$id/in-use'
| '/emails/$id/verify'
@@ -402,9 +398,9 @@ export interface FileRouteTypes {
| '/devices/$'
| '/reset-cross-signing/cancelled'
| '/reset-cross-signing/success'
+ | '/sessions/$id'
| '/_account/'
| '/reset-cross-signing/'
- | '/_account/sessions/$id'
| '/_account/sessions/browsers'
| '/emails/$id/in-use'
| '/emails/$id/verify'
@@ -420,6 +416,7 @@ export interface RootRouteChildren {
ResetCrossSigningRoute: typeof ResetCrossSigningRouteWithChildren
ClientsIdRoute: typeof ClientsIdRoute
DevicesSplatRoute: typeof DevicesSplatRoute
+ SessionsIdRoute: typeof SessionsIdRoute
EmailsIdInUseRoute: typeof EmailsIdInUseRoute
EmailsIdVerifyRoute: typeof EmailsIdVerifyRoute
PasswordChangeSuccessLazyRoute: typeof PasswordChangeSuccessLazyRoute
@@ -432,6 +429,7 @@ const rootRouteChildren: RootRouteChildren = {
ResetCrossSigningRoute: ResetCrossSigningRouteWithChildren,
ClientsIdRoute: ClientsIdRoute,
DevicesSplatRoute: DevicesSplatRoute,
+ SessionsIdRoute: SessionsIdRoute,
EmailsIdInUseRoute: EmailsIdInUseRoute,
EmailsIdVerifyRoute: EmailsIdVerifyRoute,
PasswordChangeSuccessLazyRoute: PasswordChangeSuccessLazyRoute,
@@ -453,6 +451,7 @@ export const routeTree = rootRoute
"/reset-cross-signing",
"/clients/$id",
"/devices/$",
+ "/sessions/$id",
"/emails/$id/in-use",
"/emails/$id/verify",
"/password/change/success",
@@ -464,7 +463,6 @@ export const routeTree = rootRoute
"filePath": "_account.tsx",
"children": [
"/_account/",
- "/_account/sessions/$id",
"/_account/sessions/browsers",
"/_account/sessions/"
]
@@ -491,6 +489,9 @@ export const routeTree = rootRoute
"filePath": "reset-cross-signing.success.tsx",
"parent": "/reset-cross-signing"
},
+ "/sessions/$id": {
+ "filePath": "sessions.$id.tsx"
+ },
"/_account/": {
"filePath": "_account.index.tsx",
"parent": "/_account"
@@ -499,10 +500,6 @@ export const routeTree = rootRoute
"filePath": "reset-cross-signing.index.tsx",
"parent": "/reset-cross-signing"
},
- "/_account/sessions/$id": {
- "filePath": "_account.sessions.$id.tsx",
- "parent": "/_account"
- },
"/_account/sessions/browsers": {
"filePath": "_account.sessions.browsers.tsx",
"parent": "/_account"
diff --git a/frontend/src/routes/_account.index.lazy.tsx b/frontend/src/routes/_account.index.lazy.tsx
index f831f19d3..57b68eefe 100644
--- a/frontend/src/routes/_account.index.lazy.tsx
+++ b/frontend/src/routes/_account.index.lazy.tsx
@@ -10,28 +10,63 @@ import {
notFound,
useNavigate,
} from "@tanstack/react-router";
-import { Separator, Text } from "@vector-im/compound-web";
+import IconSignOut from "@vector-im/compound-design-tokens/assets/web/icons/sign-out";
+import { Button, Separator, Text } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
-
import AccountManagementPasswordPreview from "../components/AccountManagementPasswordPreview";
import { ButtonLink } from "../components/ButtonLink";
import * as Collapsible from "../components/Collapsible";
+import * as Dialog from "../components/Dialog";
+import LoadingSpinner from "../components/LoadingSpinner";
+import { useEndBrowserSession } from "../components/Session/EndBrowserSessionButton";
import AddEmailForm from "../components/UserProfile/AddEmailForm";
import UserEmailList from "../components/UserProfile/UserEmailList";
-
import { query } from "./_account.index";
export const Route = createLazyFileRoute("/_account/")({
component: Index,
});
+const SignOutButton: React.FC<{ id: string }> = ({ id }) => {
+ const { t } = useTranslation();
+ const mutation = useEndBrowserSession(id, true);
+
+ return (
+
+ Sign out of account
+
+ }
+ >
+ Sign out of your account?
+
+ mutation.mutate()}
+ disabled={mutation.isPending}
+ Icon={mutation.isPending ? undefined : IconSignOut}
+ >
+ {mutation.isPending && }
+ {t("action.sign_out")}
+
+
+
+ {t("action.cancel")}
+
+
+ );
+};
+
function Index(): React.ReactElement {
const navigate = useNavigate();
const { t } = useTranslation();
const {
- data: { viewer, siteConfig },
+ data: { viewerSession, siteConfig },
} = useSuspenseQuery(query);
- if (viewer?.__typename !== "User") throw notFound();
+ if (viewerSession?.__typename !== "BrowserSession") throw notFound();
// When adding an email, we want to go to the email verification form
const onAdd = async (id: string): Promise => {
@@ -39,45 +74,52 @@ function Index(): React.ReactElement {
};
return (
-
- {/* Only display this section if the user can add email addresses to their
+ <>
+
+ {/* Only display this section if the user can add email addresses to their
account *or* if they have any existing email addresses */}
- {(siteConfig.emailChangeAllowed || viewer.emails.totalCount > 0) && (
- <>
-
-
+ {(siteConfig.emailChangeAllowed ||
+ viewerSession.user.emails.totalCount > 0) && (
+ <>
+
+
- {siteConfig.emailChangeAllowed && }
-
+ {siteConfig.emailChangeAllowed && }
+
-
- >
- )}
+
+ >
+ )}
- {siteConfig.passwordLoginEnabled && (
- <>
-
-
-
+ {siteConfig.passwordLoginEnabled && (
+ <>
+
+
+
-
- >
- )}
+
+ >
+ )}
-
-
- {t("frontend.reset_cross_signing.description")}
-
-
- {t("frontend.reset_cross_signing.start_reset")}
-
-
-
+
+
+ {t("frontend.reset_cross_signing.description")}
+
+
+ {t("frontend.reset_cross_signing.start_reset")}
+
+
+
+
+
+
+
+ >
);
}
diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx
index bbd0a41d5..cd9a12271 100644
--- a/frontend/src/routes/_account.index.tsx
+++ b/frontend/src/routes/_account.index.tsx
@@ -13,11 +13,14 @@ import { graphqlRequest } from "../graphql";
const QUERY = graphql(/* GraphQL */ `
query UserProfile {
- viewer {
+ viewerSession {
__typename
- ... on User {
- emails(first: 0) {
- totalCount
+ ... on BrowserSession {
+ id
+ user {
+ emails(first: 0) {
+ totalCount
+ }
}
}
}
diff --git a/frontend/src/routes/_account.lazy.tsx b/frontend/src/routes/_account.lazy.tsx
index 3acf066fd..bd5e10abc 100644
--- a/frontend/src/routes/_account.lazy.tsx
+++ b/frontend/src/routes/_account.lazy.tsx
@@ -1,4 +1,4 @@
-// Copyright 2024 New Vector Ltd.
+// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
@@ -7,12 +7,9 @@
import { Outlet, createLazyFileRoute, notFound } from "@tanstack/react-router";
import { Heading } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
-
-import { useEndBrowserSession } from "../components/BrowserSession";
import Layout from "../components/Layout";
import NavBar from "../components/NavBar";
import NavItem from "../components/NavItem";
-import EndSessionButton from "../components/Session/EndSessionButton";
import UserGreeting from "../components/UserGreeting";
import { useSuspenseQuery } from "@tanstack/react-query";
@@ -25,10 +22,9 @@ export const Route = createLazyFileRoute("/_account")({
function Account(): React.ReactElement {
const { t } = useTranslation();
const result = useSuspenseQuery(query);
- const session = result.data.viewerSession;
- if (session?.__typename !== "BrowserSession") throw notFound();
+ const viewer = result.data.viewer;
+ if (viewer?.__typename !== "User") throw notFound();
const siteConfig = result.data.siteConfig;
- const onSessionEnd = useEndBrowserSession(session.id, true);
return (
@@ -37,12 +33,10 @@ function Account(): React.ReactElement {
{t("frontend.account.title")}
-
-
-
+
{t("frontend.nav.settings")}
diff --git a/frontend/src/routes/_account.sessions.browsers.lazy.tsx b/frontend/src/routes/_account.sessions.browsers.lazy.tsx
index eefcf9dbc..a1752b33c 100644
--- a/frontend/src/routes/_account.sessions.browsers.lazy.tsx
+++ b/frontend/src/routes/_account.sessions.browsers.lazy.tsx
@@ -8,7 +8,6 @@ import { createLazyFileRoute, notFound } from "@tanstack/react-router";
import { H5 } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
-import BlockList from "../components/BlockList";
import BrowserSession from "../components/BrowserSession";
import { ButtonLink } from "../components/ButtonLink";
import EmptyState from "../components/EmptyState";
@@ -42,7 +41,7 @@ function BrowserSessions(): React.ReactElement {
// We reverse the list as we are paginating backwards
const edges = [...viewerSession.user.browserSessions.edges].reverse();
return (
-
+
{t("frontend.browser_sessions_overview.heading")}
@@ -104,6 +103,6 @@ function BrowserSessions(): React.ReactElement {
)}
-
+
);
}
diff --git a/frontend/src/routes/_account.sessions.index.lazy.tsx b/frontend/src/routes/_account.sessions.index.lazy.tsx
index 4f1b8fa5a..c98e44459 100644
--- a/frontend/src/routes/_account.sessions.index.lazy.tsx
+++ b/frontend/src/routes/_account.sessions.index.lazy.tsx
@@ -8,7 +8,6 @@ import { createLazyFileRoute, notFound } from "@tanstack/react-router";
import { H3, Separator } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
-import BlockList from "../components/BlockList";
import { ButtonLink } from "../components/ButtonLink";
import CompatSession from "../components/CompatSession";
import EmptyState from "../components/EmptyState";
@@ -53,7 +52,7 @@ function Sessions(): React.ReactElement {
const edges = [...appSessions.edges].reverse();
return (
-
+
{t("frontend.user_sessions_overview.heading")}
@@ -121,6 +120,6 @@ function Sessions(): React.ReactElement {
)}
-
+
);
}
diff --git a/frontend/src/routes/_account.tsx b/frontend/src/routes/_account.tsx
index e61b98aaa..9c7bdc714 100644
--- a/frontend/src/routes/_account.tsx
+++ b/frontend/src/routes/_account.tsx
@@ -11,15 +11,10 @@ import { graphqlRequest } from "../graphql";
const QUERY = graphql(/* GraphQL */ `
query CurrentUserGreeting {
- viewerSession {
+ viewer {
__typename
-
- ... on BrowserSession {
- id
-
- user {
- ...UserGreeting_user
- }
+ ... on User {
+ ...UserGreeting_user
}
}
diff --git a/frontend/src/routes/password.change.index.lazy.tsx b/frontend/src/routes/password.change.index.lazy.tsx
index a66e6c65e..b27f94c21 100644
--- a/frontend/src/routes/password.change.index.lazy.tsx
+++ b/frontend/src/routes/password.change.index.lazy.tsx
@@ -15,7 +15,6 @@ import { Alert, Form, Separator } from "@vector-im/compound-web";
import { type FormEvent, useRef } from "react";
import { useTranslation } from "react-i18next";
-import BlockList from "../components/BlockList";
import { ButtonLink } from "../components/ButtonLink";
import Layout from "../components/Layout";
import LoadingSpinner from "../components/LoadingSpinner";
@@ -103,7 +102,7 @@ function ChangePassword(): React.ReactNode {
return (
-
+
);
}
diff --git a/frontend/src/routes/password.change.success.lazy.tsx b/frontend/src/routes/password.change.success.lazy.tsx
index 1638189e6..15357e0e0 100644
--- a/frontend/src/routes/password.change.success.lazy.tsx
+++ b/frontend/src/routes/password.change.success.lazy.tsx
@@ -7,8 +7,6 @@
import { createLazyFileRoute } from "@tanstack/react-router";
import IconCheckCircle from "@vector-im/compound-design-tokens/assets/web/icons/check-circle-solid";
import { useTranslation } from "react-i18next";
-
-import BlockList from "../components/BlockList";
import { ButtonLink } from "../components/ButtonLink";
import Layout from "../components/Layout";
import PageHeading from "../components/PageHeading";
@@ -22,7 +20,7 @@ function ChangePasswordSuccess(): React.ReactNode {
return (
-
+
);
}
diff --git a/frontend/src/routes/password.recovery.index.lazy.tsx b/frontend/src/routes/password.recovery.index.lazy.tsx
index 3b16d6d1a..c97d7ab53 100644
--- a/frontend/src/routes/password.recovery.index.lazy.tsx
+++ b/frontend/src/routes/password.recovery.index.lazy.tsx
@@ -16,8 +16,6 @@ import IconLockSolid from "@vector-im/compound-design-tokens/assets/web/icons/lo
import { Alert, Button, Form } from "@vector-im/compound-web";
import type { FormEvent } from "react";
import { useTranslation } from "react-i18next";
-
-import BlockList from "../components/BlockList";
import { ButtonLink } from "../components/ButtonLink";
import Layout from "../components/Layout";
import LoadingSpinner from "../components/LoadingSpinner";
@@ -206,7 +204,7 @@ const EmailRecovery: React.FC<{
return (
-
+
);
};
diff --git a/frontend/src/routes/reset-cross-signing.index.tsx b/frontend/src/routes/reset-cross-signing.index.tsx
index db7150faf..ce83780cf 100644
--- a/frontend/src/routes/reset-cross-signing.index.tsx
+++ b/frontend/src/routes/reset-cross-signing.index.tsx
@@ -13,15 +13,16 @@ import { createFileRoute, notFound } from "@tanstack/react-router";
import IconCheck from "@vector-im/compound-design-tokens/assets/web/icons/check";
import IconError from "@vector-im/compound-design-tokens/assets/web/icons/error";
import IconInfo from "@vector-im/compound-design-tokens/assets/web/icons/info";
-import { Button, Text } from "@vector-im/compound-web";
+import {
+ Button,
+ Text,
+ VisualList,
+ VisualListItem,
+} from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import { ButtonLink } from "../components/ButtonLink";
import LoadingSpinner from "../components/LoadingSpinner";
import PageHeading from "../components/PageHeading";
-import {
- VisualList,
- VisualListItem,
-} from "../components/VisualList/VisualList";
import { graphql } from "../gql";
import { graphqlRequest } from "../graphql";
@@ -123,19 +124,15 @@ function ResetCrossSigning(): React.ReactNode {
-
-
-
+
+ {t("frontend.reset_cross_signing.effect_list.positive_1")}
+
+
+ {t("frontend.reset_cross_signing.effect_list.neutral_1")}
+
+
+ {t("frontend.reset_cross_signing.effect_list.neutral_2")}
+
diff --git a/frontend/src/routes/reset-cross-signing.tsx b/frontend/src/routes/reset-cross-signing.tsx
index e657d34ed..6ca2c32d4 100644
--- a/frontend/src/routes/reset-cross-signing.tsx
+++ b/frontend/src/routes/reset-cross-signing.tsx
@@ -13,7 +13,6 @@ import { Button, Text } from "@vector-im/compound-web";
import * as v from "valibot";
import { useTranslation } from "react-i18next";
-import BlockList from "../components/BlockList";
import Layout from "../components/Layout";
import PageHeading from "../components/PageHeading";
@@ -25,9 +24,9 @@ export const Route = createFileRoute("/reset-cross-signing")({
validateSearch: searchSchema,
component: () => (
-
+
-
+
),
errorComponent: ResetCrossSigningError,
diff --git a/frontend/src/routes/_account.sessions.$id.lazy.tsx b/frontend/src/routes/sessions.$id.lazy.tsx
similarity index 61%
rename from frontend/src/routes/_account.sessions.$id.lazy.tsx
rename to frontend/src/routes/sessions.$id.lazy.tsx
index 35e3201a9..bc9524bf2 100644
--- a/frontend/src/routes/_account.sessions.$id.lazy.tsx
+++ b/frontend/src/routes/sessions.$id.lazy.tsx
@@ -4,19 +4,18 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
+import { useSuspenseQuery } from "@tanstack/react-query";
import { createLazyFileRoute, notFound } from "@tanstack/react-router";
import { Alert } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
-
+import Layout from "../components/Layout";
import { Link } from "../components/Link";
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
import CompatSessionDetail from "../components/SessionDetail/CompatSessionDetail";
import OAuth2SessionDetail from "../components/SessionDetail/OAuth2SessionDetail";
+import { query } from "./sessions.$id";
-import { useSuspenseQuery } from "@tanstack/react-query";
-import { query } from "./_account.sessions.$id";
-
-export const Route = createLazyFileRoute("/_account/sessions/$id")({
+export const Route = createLazyFileRoute("/sessions/$id")({
notFoundComponent: NotFound,
component: SessionDetail,
});
@@ -26,13 +25,15 @@ function NotFound(): React.ReactElement {
const { t } = useTranslation();
return (
-
- {t("frontend.session_detail.alert.text")}
- {t("frontend.session_detail.alert.button")}
-
+
+
+ {t("frontend.session_detail.alert.text")}
+ {t("frontend.session_detail.alert.button")}
+
+
);
}
@@ -45,15 +46,25 @@ function SessionDetail(): React.ReactElement {
switch (node.__typename) {
case "CompatSession":
- return ;
+ return (
+
+
+
+ );
case "Oauth2Session":
- return ;
+ return (
+
+
+
+ );
case "BrowserSession":
return (
-
+
+
+
);
default:
throw new Error("Unknown session type");
diff --git a/frontend/src/routes/_account.sessions.$id.tsx b/frontend/src/routes/sessions.$id.tsx
similarity index 93%
rename from frontend/src/routes/_account.sessions.$id.tsx
rename to frontend/src/routes/sessions.$id.tsx
index b1d4668d3..9139aed31 100644
--- a/frontend/src/routes/_account.sessions.$id.tsx
+++ b/frontend/src/routes/sessions.$id.tsx
@@ -34,7 +34,7 @@ export const query = (id: string) =>
graphqlRequest({ query: QUERY, signal, variables: { id } }),
});
-export const Route = createFileRoute("/_account/sessions/$id")({
+export const Route = createFileRoute("/sessions/$id")({
loader: ({ context, params }) =>
context.queryClient.ensureQueryData(query(params.id)),
});
diff --git a/frontend/src/utils/simplifyUrl.ts b/frontend/src/utils/simplifyUrl.ts
new file mode 100644
index 000000000..2319ebfb0
--- /dev/null
+++ b/frontend/src/utils/simplifyUrl.ts
@@ -0,0 +1,33 @@
+// Copyright 2025 New Vector Ltd.
+//
+// SPDX-License-Identifier: AGPL-3.0-only
+// Please see LICENSE in the repository root for full details.
+
+/**
+ * Simplify a URL by removing the protocol, search params and hash.
+ *
+ * @param url The URL to simplify
+ * @returns The simplified URL
+ */
+const simplifyUrl = (url: string): string => {
+ let parsed: URL;
+ try {
+ parsed = new URL(url);
+ } catch (_e) {
+ // Not a valid URL, return the original
+ return url;
+ }
+
+ // Clear out the search params and hash
+ parsed.search = "";
+ parsed.hash = "";
+
+ if (parsed.protocol === "https:") {
+ return parsed.hostname;
+ }
+
+ // Return the simplified URL
+ return parsed.toString();
+};
+
+export default simplifyUrl;
diff --git a/frontend/stories/routes/index.stories.tsx b/frontend/stories/routes/index.stories.tsx
index 9156ff0fa..15fb4dedd 100644
--- a/frontend/stories/routes/index.stories.tsx
+++ b/frontend/stories/routes/index.stories.tsx
@@ -43,10 +43,13 @@ const userProfileHandler = ({
mockUserProfileQuery(() =>
HttpResponse.json({
data: {
- viewer: {
- __typename: "User",
- emails: {
- totalCount: emailTotalCount,
+ viewerSession: {
+ __typename: "BrowserSession",
+ id: "session-id",
+ user: {
+ emails: {
+ totalCount: emailTotalCount,
+ },
},
},
diff --git a/frontend/tests/mocks/handlers.ts b/frontend/tests/mocks/handlers.ts
index dcba3b5cc..b89347ccc 100644
--- a/frontend/tests/mocks/handlers.ts
+++ b/frontend/tests/mocks/handlers.ts
@@ -60,23 +60,19 @@ export const handlers = [
mockCurrentUserGreetingQuery(() =>
HttpResponse.json({
data: {
- viewerSession: {
- __typename: "BrowserSession",
-
- id: "session-id",
- user: Object.assign(
- makeFragmentData(
- {
- id: "user-id",
- matrix: {
- mxid: "@alice:example.com",
- displayName: "Alice",
- },
+ viewer: Object.assign(
+ makeFragmentData(
+ {
+ __typename: "User",
+ id: "user-id",
+ matrix: {
+ mxid: "@alice:example.com",
+ displayName: "Alice",
},
- USER_GREETING_FRAGMENT,
- ),
+ },
+ USER_GREETING_FRAGMENT,
),
- },
+ ),
siteConfig: makeFragmentData(
{
@@ -91,10 +87,13 @@ export const handlers = [
mockUserProfileQuery(() =>
HttpResponse.json({
data: {
- viewer: {
- __typename: "User",
- emails: {
- totalCount: 1,
+ viewerSession: {
+ __typename: "BrowserSession",
+ id: "browser-session-id",
+ user: {
+ emails: {
+ totalCount: 1,
+ },
},
},
diff --git a/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap
index 5964df356..b73ec4ea9 100644
--- a/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap
+++ b/frontend/tests/routes/__snapshots__/reset-cross-signing.test.tsx.snap
@@ -6,7 +6,7 @@ exports[`Reset cross signing > renders the cancelled page 1`] = `
class="_layoutContainer_0c8bf9"
>
renders the deep link page 1`] = `
class="_layoutContainer_0c8bf9"
>
renders the deep link page 1`] = `
If you're not signed in to any other devices and you've lost your recovery key, then you'll need to reset your identity to continue using the app.
-
- Your account details, contacts, preferences, and chat list will be kept
-
+ Your account details, contacts, preferences, and chat list will be kept
renders the deep link page 1`] = `
fill-rule="evenodd"
/>
-
- You will lose any message history that's stored only on the server
-
+ You will lose any message history that's stored only on the server
renders the deep link page 1`] = `
fill-rule="evenodd"
/>
-
- You will need to verify all your existing devices and contacts again
-
+ You will need to verify all your existing devices and contacts again
renders the page 1`] = `
class="_layoutContainer_0c8bf9"
>
renders the page 1`] = `
If you're not signed in to any other devices and you've lost your recovery key, then you'll need to reset your identity to continue using the app.
-
- Your account details, contacts, preferences, and chat list will be kept
-
+ Your account details, contacts, preferences, and chat list will be kept
renders the page 1`] = `
fill-rule="evenodd"
/>
-
- You will lose any message history that's stored only on the server
-
+ You will lose any message history that's stored only on the server
renders the page 1`] = `
fill-rule="evenodd"
/>
-
- You will need to verify all your existing devices and contacts again
-
+ You will need to verify all your existing devices and contacts again
renders the success page 1`] = `
class="_layoutContainer_0c8bf9"
>
renders the success page 2`] = `
class="_layoutContainer_0c8bf9"
>
display name edit box > displays an error if the display name is invalid 1`] = `
Edit profile
@@ -150,18 +150,18 @@ exports[`Account home page > display name edit box > displays an error if the di
exports[`Account home page > display name edit box > lets edit the display name 1`] = `
Edit profile
@@ -308,32 +308,6 @@ exports[`Account home page > renders the page 1`] = `
>
Your account
-
-
-
-
- Sign out
-
renders the page 1`] = `
renders the page 1`] = `
@@ -445,14 +419,14 @@ exports[`Account home page > renders the page 1`] = `
>
Contact info
renders the page 1`] = `
Add an alternative email you can use to access this account.
@@ -554,7 +528,7 @@ exports[`Account home page > renders the page 1`] = `
role="separator"
/>
@@ -566,14 +540,14 @@ exports[`Account home page > renders the page 1`] = `
>
Account password
renders the page 1`] = `
+
+
+
+
+
+ Sign out of account
+