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

frontend: split the various lists off the home page

This commit is contained in:
Quentin Gliech
2023-08-08 17:06:39 +02:00
parent 0ea574f57e
commit 3feb9113bb
16 changed files with 752 additions and 56 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2022 The Matrix.org Foundation C.I.C.
// Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -25,17 +25,23 @@ type Location = {
};
type HomeRoute = { type: "home" };
type AccountRoute = { type: "account" };
type EmailListRoute = { type: "email-list" };
type OAuth2ClientRoute = { type: "client"; id: string };
type OAuth2SessionList = { type: "oauth2-session-list" };
type BrowserSessionRoute = { type: "session"; id: string };
type BrowserSessionListRoute = { type: "browser-session-list" };
type CompatSessionListRoute = { type: "compat-session-list" };
type VerifyEmailRoute = { type: "verify-email"; id: string };
type UnknownRoute = { type: "unknown"; segments: string[] };
export type Route =
| HomeRoute
| AccountRoute
| EmailListRoute
| OAuth2ClientRoute
| OAuth2SessionList
| BrowserSessionRoute
| BrowserSessionListRoute
| CompatSessionListRoute
| VerifyEmailRoute
| UnknownRoute;
@@ -43,40 +49,84 @@ const routeToSegments = (route: Route): string[] => {
switch (route.type) {
case "home":
return [];
case "account":
return ["account"];
case "client":
return ["client", route.id];
case "session":
return ["session", route.id];
case "email-list":
return ["emails"];
case "verify-email":
return ["verify-email", route.id];
return ["emails", route.id, "verify"];
case "client":
return ["clients", route.id];
case "browser-session-list":
return ["sessions"];
case "session":
return ["sessions", route.id];
case "oauth2-session-list":
return ["oauth2-sessions"];
case "compat-session-list":
return ["compat-sessions"];
case "unknown":
return route.segments;
}
};
const P = Symbol();
type PatternItem = string | typeof P;
// Returns true if the segments match the pattern, where P is a parameter
const segmentMatches = (
segments: string[],
...pattern: PatternItem[]
): boolean => {
// Quick check to see if the lengths match
if (segments.length !== pattern.length) return false;
// Check each segment
for (let i = 0; i < segments.length; i++) {
// If the pattern is P, then it's a parameter and we can skip it
if (pattern[i] === P) continue;
// Otherwise, check that the segment matches the pattern
if (segments[i] !== pattern[i]) return false;
}
return true;
};
const segmentsToRoute = (segments: string[]): Route => {
const matches = (...pattern: PatternItem[]): boolean =>
segmentMatches(segments, ...pattern);
// Special case for the home page
if (segments.length === 0 || (segments.length === 1 && segments[0] === "")) {
return { type: "home" };
}
if (segments.length === 1 && segments[0] === "account") {
return { type: "account" };
if (matches("emails")) {
return { type: "email-list" };
}
if (segments.length === 2 && segments[0] === "client") {
if (matches("sessions")) {
return { type: "browser-session-list" };
}
if (matches("oauth2-sessions")) {
return { type: "oauth2-session-list" };
}
if (matches("compat-sessions")) {
return { type: "compat-session-list" };
}
if (matches("emails", P, "verify")) {
return { type: "verify-email", id: segments[1] };
}
if (matches("clients", P)) {
return { type: "client", id: segments[1] };
}
if (segments.length === 2 && segments[0] === "session") {
if (matches("sessions", P)) {
return { type: "session", id: segments[1] };
}
if (segments.length === 2 && segments[0] === "verify-email") {
return { type: "verify-email", id: segments[1] };
}
return { type: "unknown", segments };
};
@@ -117,9 +167,12 @@ export const routeAtom = atom(
);
const Home = lazy(() => import("./pages/Home"));
const Account = lazy(() => import("./pages/Account"));
const EmailList = lazy(() => import("./pages/EmailList"));
const OAuth2Client = lazy(() => import("./pages/OAuth2Client"));
const BrowserSession = lazy(() => import("./pages/BrowserSession"));
const BrowserSessionList = lazy(() => import("./pages/BrowserSessionList"));
const CompatSessionList = lazy(() => import("./pages/CompatSessionList"));
const OAuth2SessionList = lazy(() => import("./pages/OAuth2SessionList"));
const VerifyEmail = lazy(() => import("./pages/VerifyEmail"));
const InnerRouter: React.FC = () => {
@@ -128,8 +181,14 @@ const InnerRouter: React.FC = () => {
switch (route.type) {
case "home":
return <Home />;
case "account":
return <Account />;
case "email-list":
return <EmailList />;
case "oauth2-session-list":
return <OAuth2SessionList />;
case "browser-session-list":
return <BrowserSessionList />;
case "compat-session-list":
return <CompatSessionList />;
case "client":
return <OAuth2Client id={route.id} />;
case "session":
@@ -158,7 +217,6 @@ const shouldHandleClick = (e: React.MouseEvent): boolean =>
export const Link: React.FC<
{
route: Route;
children: React.ReactNode;
} & React.HTMLProps<HTMLAnchorElement>
> = ({ route, children, ...props }) => {
const config = useAtomValue(appConfigAtom);

View File

@@ -42,8 +42,8 @@ const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
<UserGreeting userId={userId} />
<NavBar>
<NavItem route={{ type: "home" }}>Sessions</NavItem>
<NavItem route={{ type: "account" }}>Emails</NavItem>
<NavItem route={{ type: "home" }}>Home</NavItem>
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
</NavBar>
<main>{children}</main>

View File

@@ -30,7 +30,7 @@ const meta = {
<WithHomePage>
<NavBar {...props}>
<NavItem route={{ type: "home" }}>Home</NavItem>
<NavItem route={{ type: "account" }}>Account</NavItem>
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
<ExternalLink href="https://example.com">External</ExternalLink>
</NavBar>
</WithHomePage>

View File

@@ -53,7 +53,7 @@ export const Active: Story = {
export const Inactive: Story = {
args: {
route: { type: "account" },
children: "Account",
route: { type: "email-list" },
children: "Emails",
},
};

View File

@@ -77,7 +77,7 @@ describe("NavItem", () => {
it("renders a different route", () => {
const component = create(
<WithLocation path="/">
<NavItem route={{ type: "account" }}>Account</NavItem>
<NavItem route={{ type: "email-list" }}>Emails</NavItem>
</WithLocation>,
);
const tree = component.toJSON();

View File

@@ -8,7 +8,7 @@ exports[`NavItem > render an active <NavItem /> 1`] = `
<a
aria-current="page"
className="_navItem_8603fc"
href=""
href="/"
onClick={[Function]}
>
Active
@@ -22,7 +22,7 @@ exports[`NavItem > render an inactive <NavItem /> 1`] = `
>
<a
className="_navItem_8603fc"
href=""
href="/"
onClick={[Function]}
>
Inactive
@@ -36,10 +36,10 @@ exports[`NavItem > renders a different route 1`] = `
>
<a
className="_navItem_8603fc"
href="account"
href="/emails"
onClick={[Function]}
>
Account
Emails
</a>
</li>
`;

View File

@@ -0,0 +1,101 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Alert, Body } from "@vector-im/compound-web";
import { useState } from "react";
import { Link } from "../../Router";
import { FragmentType, graphql, useFragment } from "../../gql";
import UserEmail from "../UserEmail";
const FRAGMENT = graphql(/* GraphQL */ `
fragment UserHome_user on User {
id
primaryEmail {
id
...UserEmail_email
}
confirmedEmails: emails(first: 0, state: CONFIRMED) {
totalCount
}
unverifiedEmails: emails(first: 0, state: PENDING) {
totalCount
}
browserSessions(first: 0, state: ACTIVE) {
totalCount
}
oauth2Sessions(first: 0, state: ACTIVE) {
totalCount
}
compatSessions(first: 0, state: ACTIVE) {
totalCount
}
}
`);
const UserHome: React.FC<{
user: FragmentType<typeof FRAGMENT>;
}> = ({ user }) => {
const data = useFragment(FRAGMENT, user);
const [dismiss, setDismiss] = useState(false);
const doDismiss = (): void => {
setDismiss(true);
};
return (
<>
{data.unverifiedEmails.totalCount > 0 && !dismiss && (
<Alert type="critical" title="Unverified email" onClose={doDismiss}>
You have {data.unverifiedEmails.totalCount} unverified email
address(es). <Link route={{ type: "email-list" }}>Check</Link>
</Alert>
)}
{data.primaryEmail ? (
<UserEmail email={data.primaryEmail} isPrimary />
) : (
<Alert type="critical" title="No primary email adress" />
)}
{data.confirmedEmails.totalCount > 1 && (
<Body>
{data.confirmedEmails.totalCount} additional emails.{" "}
<Link route={{ type: "email-list" }}>View all</Link>
</Body>
)}
<Body>
{data.browserSessions.totalCount} active browser session(s).{" "}
<Link route={{ type: "browser-session-list" }}>View all</Link>
</Body>
<Body>
{data.oauth2Sessions.totalCount} active OAuth2 session(s).{" "}
<Link route={{ type: "oauth2-session-list" }}>View all</Link>
</Body>
<Body>
{data.compatSessions.totalCount} active compatibility layer session(s).{" "}
<Link route={{ type: "compat-session-list" }}>View all</Link>
</Body>
</>
);
};
export default UserHome;

View File

@@ -0,0 +1,15 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
export { default } from "./UserHome";

View File

@@ -127,7 +127,7 @@ const VerifyEmail: React.FC<{
e.currentTarget?.reset();
if (result.data?.verifyEmail.status === "VERIFIED") {
setRoute({ type: "account" });
setRoute({ type: "email-list" });
} else {
fieldRef.current?.focus();
fieldRef.current?.select();

View File

@@ -51,6 +51,8 @@ const documents = {
types.UserPrimaryEmailDocument,
"\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n":
types.UserGreetingDocument,
"\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n":
types.UserHome_UserFragmentDoc,
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n":
types.UserEmail_VerifyEmailFragmentDoc,
"\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
@@ -59,6 +61,8 @@ const documents = {
types.ResendVerificationEmailDocument,
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
types.BrowserSessionQueryDocument,
"\n query HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n":
types.HomeQueryDocument,
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n }\n }\n":
types.OAuth2ClientQueryDocument,
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
@@ -193,6 +197,12 @@ export function graphql(
export function graphql(
source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n",
): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n matrix {\n mxid\n displayName\n }\n }\n }\n"];
/**
* 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 UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n",
): (typeof documents)["\n fragment UserHome_user on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n confirmedEmails: emails(first: 0, state: CONFIRMED) {\n totalCount\n }\n\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n oauth2Sessions(first: 0, state: ACTIVE) {\n totalCount\n }\n\n compatSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -217,6 +227,12 @@ export function graphql(
export function graphql(
source: "\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n",
): (typeof documents)["\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n"];
/**
* 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 HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n",
): (typeof documents)["\n query HomeQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserHome_user\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1231,6 +1231,30 @@ export type UserGreetingQuery = {
} | null;
};
export type UserHome_UserFragment = {
__typename?: "User";
id: string;
primaryEmail?:
| ({ __typename?: "UserEmail"; id: string } & {
" $fragmentRefs"?: { UserEmail_EmailFragment: UserEmail_EmailFragment };
})
| null;
confirmedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number };
browserSessions: {
__typename?: "BrowserSessionConnection";
totalCount: number;
};
oauth2Sessions: {
__typename?: "Oauth2SessionConnection";
totalCount: number;
};
compatSessions: {
__typename?: "CompatSessionConnection";
totalCount: number;
};
} & { " $fragmentName"?: "UserHome_UserFragment" };
export type UserEmail_VerifyEmailFragment = {
__typename?: "UserEmail";
id: string;
@@ -1301,6 +1325,17 @@ export type BrowserSessionQueryQuery = {
} | null;
};
export type HomeQueryQueryVariables = Exact<{ [key: string]: never }>;
export type HomeQueryQuery = {
__typename?: "Query";
viewer:
| { __typename: "Anonymous" }
| ({ __typename: "User"; id: string } & {
" $fragmentRefs"?: { UserHome_UserFragment: UserHome_UserFragment };
});
};
export type OAuth2ClientQueryQueryVariables = Exact<{
id: Scalars["ID"]["input"];
}>;
@@ -1492,6 +1527,167 @@ export const UserEmail_EmailFragmentDoc = {
},
],
} as unknown as DocumentNode<UserEmail_EmailFragment, unknown>;
export const UserHome_UserFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserHome_user" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
{
kind: "Field",
alias: { kind: "Name", value: "confirmedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "CONFIRMED" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "browserSessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "oauth2Sessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "compatSessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
],
} as unknown as DocumentNode<UserHome_UserFragment, unknown>;
export const UserEmail_VerifyEmailFragmentDoc = {
kind: "Document",
definitions: [
@@ -3490,6 +3686,204 @@ export const BrowserSessionQueryDocument = {
BrowserSessionQueryQuery,
BrowserSessionQueryQueryVariables
>;
export const HomeQueryDocument = {
kind: "Document",
definitions: [
{
kind: "OperationDefinition",
operation: "query",
name: { kind: "Name", value: "HomeQuery" },
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "Field",
name: { kind: "Name", value: "viewer" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
{
kind: "InlineFragment",
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserHome_user" },
},
],
},
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserEmail_email" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "UserEmail" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "email" } },
{ kind: "Field", name: { kind: "Name", value: "confirmedAt" } },
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "UserHome_user" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "User" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "Field",
name: { kind: "Name", value: "primaryEmail" },
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{
kind: "FragmentSpread",
name: { kind: "Name", value: "UserEmail_email" },
},
],
},
},
{
kind: "Field",
alias: { kind: "Name", value: "confirmedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "CONFIRMED" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
alias: { kind: "Name", value: "unverifiedEmails" },
name: { kind: "Name", value: "emails" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "PENDING" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "browserSessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "oauth2Sessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
{
kind: "Field",
name: { kind: "Name", value: "compatSessions" },
arguments: [
{
kind: "Argument",
name: { kind: "Name", value: "first" },
value: { kind: "IntValue", value: "0" },
},
{
kind: "Argument",
name: { kind: "Name", value: "state" },
value: { kind: "EnumValue", value: "ACTIVE" },
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "totalCount" } },
],
},
},
],
},
},
],
} as unknown as DocumentNode<HomeQueryQuery, HomeQueryQueryVariables>;
export const OAuth2ClientQueryDocument = {
kind: "Document",
definitions: [

View File

@@ -0,0 +1,33 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { useAtomValue } from "jotai";
import { currentUserIdAtom } from "../atoms";
import List from "../components/BrowserSessionList";
import GraphQLError from "../components/GraphQLError";
import NotLoggedIn from "../components/NotLoggedIn";
import { isErr, unwrapErr, unwrapOk } from "../result";
const BrowserSessionList: React.FC = () => {
const result = useAtomValue(currentUserIdAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return <List userId={userId} />;
};
export default BrowserSessionList;

View File

@@ -0,0 +1,33 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { useAtomValue } from "jotai";
import { currentUserIdAtom } from "../atoms";
import List from "../components/CompatSessionList";
import GraphQLError from "../components/GraphQLError";
import NotLoggedIn from "../components/NotLoggedIn";
import { isErr, unwrapErr, unwrapOk } from "../result";
const CompatSessionList: React.FC = () => {
const result = useAtomValue(currentUserIdAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return <List userId={userId} />;
};
export default CompatSessionList;

View File

@@ -20,18 +20,14 @@ import NotLoggedIn from "../components/NotLoggedIn";
import UserEmailList from "../components/UserEmailList";
import { isErr, unwrapErr, unwrapOk } from "../result";
const UserAccount: React.FC<{ id: string }> = ({ id }) => {
return <UserEmailList userId={id} />;
};
const CurrentUserAccount: React.FC = () => {
const EmailList: React.FC = () => {
const result = useAtomValue(currentUserIdAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return <UserAccount id={userId} />;
return <UserEmailList userId={userId} />;
};
export default CurrentUserAccount;
export default EmailList;

View File

@@ -13,31 +13,48 @@
// limitations under the License.
import { useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-urql";
import { currentUserIdAtom } from "../atoms";
import BrowserSessionList from "../components/BrowserSessionList";
import CompatSessionList from "../components/CompatSessionList";
import { mapQueryAtom } from "../atoms";
import GraphQLError from "../components/GraphQLError";
import NotLoggedIn from "../components/NotLoggedIn";
import OAuth2SessionList from "../components/OAuth2SessionList";
import UserHome from "../components/UserHome";
import { graphql } from "../gql";
import { isErr, unwrapErr, unwrapOk } from "../result";
const QUERY = graphql(/* GraphQL */ `
query HomeQuery {
viewer {
__typename
... on User {
id
...UserHome_user
}
}
}
`);
const homeQueryAtom = atomWithQuery({
query: QUERY,
});
const homeAtom = mapQueryAtom(homeQueryAtom, (data) => {
if (data.viewer?.__typename === "User") {
return data.viewer;
}
return null;
});
const Home: React.FC = () => {
const result = useAtomValue(currentUserIdAtom);
const result = useAtomValue(homeAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const currentUserId = unwrapOk(result);
if (currentUserId === null) return <NotLoggedIn />;
const data = unwrapOk(result);
if (data === null) return <NotLoggedIn />;
return (
<>
<div className="mt-4 grid gap-8">
<OAuth2SessionList userId={currentUserId} />
<CompatSessionList userId={currentUserId} />
<BrowserSessionList userId={currentUserId} />
</div>
</>
);
return <UserHome user={data} />;
};
export default Home;

View File

@@ -0,0 +1,33 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { useAtomValue } from "jotai";
import { currentUserIdAtom } from "../atoms";
import GraphQLError from "../components/GraphQLError";
import NotLoggedIn from "../components/NotLoggedIn";
import List from "../components/OAuth2SessionList";
import { isErr, unwrapErr, unwrapOk } from "../result";
const OAuth2SessionList: React.FC = () => {
const result = useAtomValue(currentUserIdAtom);
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const userId = unwrapOk(result);
if (userId === null) return <NotLoggedIn />;
return <List userId={userId} />;
};
export default OAuth2SessionList;