1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-06 06:02:40 +03:00

WIP my account page

This commit is contained in:
Quentin Gliech
2023-04-27 16:38:02 +02:00
parent 574514638e
commit 3c933a9d29
14 changed files with 711 additions and 860 deletions

View File

@@ -28,6 +28,7 @@ export const HydrateAtoms = ({ children }: { children: ReactElement }) => {
const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ ` const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
query CurrentViewerQuery { query CurrentViewerQuery {
viewer { viewer {
__typename
... on User { ... on User {
id id
} }
@@ -52,6 +53,7 @@ export const currentUserIdAtom = atom(async (get) => {
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ ` const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
query CurrentViewerSessionQuery { query CurrentViewerSessionQuery {
viewerSession { viewerSession {
__typename
... on BrowserSession { ... on BrowserSession {
id id
} }

View File

@@ -14,12 +14,13 @@
import React, { useRef, useTransition } from "react"; import React, { useRef, useTransition } from "react";
import { atomWithMutation } from "jotai-urql"; import { atomWithMutation } from "jotai-urql";
import { useAtom } from "jotai"; import { useAtom, useSetAtom } from "jotai";
import { graphql } from "../gql"; import { graphql } from "../gql";
import Button from "./Button"; import Button from "./Button";
import UserEmail from "./UserEmail"; import UserEmail from "./UserEmail";
import Input from "./Input"; import Input from "./Input";
import Typography from "./Typography"; import Typography from "./Typography";
import { emailPageResultFamily } from "./UserEmailList";
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ ` const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
mutation AddEmail($userId: ID!, $email: String!) { mutation AddEmail($userId: ID!, $email: String!) {
@@ -42,12 +43,15 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
const formRef = useRef<HTMLFormElement>(null); const formRef = useRef<HTMLFormElement>(null);
const [addEmailResult, addEmail] = useAtom(addUserEmailAtom); const [addEmailResult, addEmail] = useAtom(addUserEmailAtom);
const [pending, startTransition] = useTransition(); const [pending, startTransition] = useTransition();
// XXX: is this the right way to do this?
const refetchList = useSetAtom(emailPageResultFamily(userId));
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
const email = e.currentTarget.email.value; const email = e.currentTarget.email.value;
startTransition(() => { startTransition(() => {
addEmail({ userId, email }).then(() => { addEmail({ userId, email }).then(() => {
refetchList();
if (formRef.current) { if (formRef.current) {
formRef.current.reset(); formRef.current.reset();
} }
@@ -59,7 +63,7 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
<> <>
{addEmailResult.data?.addEmail.status === "ADDED" && ( {addEmailResult.data?.addEmail.status === "ADDED" && (
<> <>
<div className="p-4"> <div className="pt-4">
<Typography variant="subtitle">Email added!</Typography> <Typography variant="subtitle">Email added!</Typography>
</div> </div>
<UserEmail email={addEmailResult.data?.addEmail.email} /> <UserEmail email={addEmailResult.data?.addEmail.email} />
@@ -67,7 +71,7 @@ const AddEmailForm: React.FC<{ userId: string }> = ({ userId }) => {
)} )}
{addEmailResult.data?.addEmail.status === "EXISTS" && ( {addEmailResult.data?.addEmail.status === "EXISTS" && (
<> <>
<div className="p-4"> <div className="pt-4">
<Typography variant="subtitle">Email already exists!</Typography> <Typography variant="subtitle">Email already exists!</Typography>
</div> </div>
<UserEmail email={addEmailResult.data?.addEmail.email} /> <UserEmail email={addEmailResult.data?.addEmail.email} />

View File

@@ -15,42 +15,58 @@
import BlockList from "./BlockList"; import BlockList from "./BlockList";
import BrowserSession from "./BrowserSession"; import BrowserSession from "./BrowserSession";
import { Title } from "./Typography"; import { Title } from "./Typography";
import { FragmentType, graphql, useFragment } from "../gql"; import { graphql } from "../gql";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useAtomValue } from "jotai";
import { currentBrowserSessionIdAtom } from "../atoms";
const FRAGMENT = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
fragment BrowserSessionList_user on User { query BrowserSessionList($userId: ID!) {
browserSessions(first: 10) { user(id: $userId) {
edges { id
cursor browserSessions(first: 10) {
node { edges {
id cursor
...BrowserSession_session node {
id
...BrowserSession_session
}
} }
} }
} }
} }
`); `);
type Props = { const browserSessionListFamily = atomFamily((userId: string) => {
user: FragmentType<typeof FRAGMENT>; const browserSessionList = atomWithQuery({
currentSessionId: string; query: QUERY,
}; getVariables: () => ({ userId }),
});
return browserSessionList;
});
const BrowserSessionList: React.FC<Props> = ({ user, currentSessionId }) => { const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
const data = useFragment(FRAGMENT, user); const result = useAtomValue(browserSessionListFamily(userId));
const currentSessionId = useAtomValue(currentBrowserSessionIdAtom);
return ( if (result.data?.user?.browserSessions) {
<BlockList> const data = result.data.user.browserSessions;
<Title>List of browser sessions:</Title> return (
{data.browserSessions.edges.map((n) => ( <BlockList>
<BrowserSession <Title>List of browser sessions:</Title>
key={n.cursor} {data.edges.map((n) => (
session={n.node} <BrowserSession
isCurrent={n.node.id === currentSessionId} key={n.cursor}
/> session={n.node}
))} isCurrent={n.node.id === currentSessionId}
</BlockList> />
); ))}
</BlockList>
);
}
return <>Failed to load browser sessions</>;
}; };
export default BrowserSessionList; export default BrowserSessionList;

View File

@@ -15,36 +15,52 @@
import BlockList from "./BlockList"; import BlockList from "./BlockList";
import CompatSsoLogin from "./CompatSsoLogin"; import CompatSsoLogin from "./CompatSsoLogin";
import { Title } from "./Typography"; import { Title } from "./Typography";
import { FragmentType, graphql, useFragment } from "../gql"; import { graphql } from "../gql";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useAtomValue } from "jotai";
const FRAGMENT = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
fragment CompatSsoLoginList_user on User { query CompatSsoLoginList($userId: ID!) {
compatSsoLogins(first: 10) { user(id: $userId) {
edges { id
node { compatSsoLogins(first: 10) {
id edges {
...CompatSsoLogin_login node {
id
...CompatSsoLogin_login
}
} }
} }
} }
} }
`); `);
type Props = { const compatSsoLoginListFamily = atomFamily((userId: string) => {
user: FragmentType<typeof FRAGMENT>; const compatSsoLoginList = atomWithQuery({
}; query: QUERY,
getVariables: () => ({ userId }),
});
const CompatSsoLoginList: React.FC<Props> = ({ user }) => { return compatSsoLoginList;
const data = useFragment(FRAGMENT, user); });
return ( const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => {
<BlockList> const result = useAtomValue(compatSsoLoginListFamily(userId));
<Title>List of compatibility sessions:</Title>
{data.compatSsoLogins.edges.map((n) => ( if (result.data?.user?.compatSsoLogins) {
<CompatSsoLogin login={n.node} key={n.node.id} /> const data = result.data.user.compatSsoLogins;
))} return (
</BlockList> <BlockList>
); <Title>List of compatibility sessions:</Title>
{data.edges.map((n) => (
<CompatSsoLogin login={n.node} key={n.node.id} />
))}
</BlockList>
);
}
return <>Failed to load list of compatibility sessions.</>;
}; };
export default CompatSsoLoginList; export default CompatSsoLoginList;

View File

@@ -12,41 +12,62 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useAtomValue } from "jotai";
import BlockList from "./BlockList"; import BlockList from "./BlockList";
import OAuth2Session from "./OAuth2Session"; import OAuth2Session from "./OAuth2Session";
import { Title } from "./Typography"; import { Title } from "./Typography";
import { FragmentType, graphql, useFragment } from "../gql"; import { graphql } from "../gql";
const FRAGMENT = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
fragment OAuth2SessionList_user on User { query OAuth2SessionListQuery($userId: ID!) {
oauth2Sessions(first: 10) { user(id: $userId) {
edges { id
cursor oauth2Sessions(first: 10) {
node { edges {
id cursor
...OAuth2Session_session node {
id
...OAuth2Session_session
}
} }
} }
} }
} }
`); `);
const oauth2SessionListFamily = atomFamily((userId: string) => {
const oauth2SessionList = atomWithQuery({
query: QUERY,
getVariables: () => ({ userId }),
});
return oauth2SessionList;
});
type Props = { type Props = {
user: FragmentType<typeof FRAGMENT>; userId: string;
}; };
const OAuth2SessionList: React.FC<Props> = ({ user }) => { const OAuth2SessionList: React.FC<Props> = ({ userId }) => {
const data = useFragment(FRAGMENT, user); const result = useAtomValue(oauth2SessionListFamily(userId));
return ( if (result.data?.user?.oauth2Sessions) {
<BlockList> const data = result.data.user.oauth2Sessions;
<Title>List of OAuth 2.0 sessions:</Title> return (
{data.oauth2Sessions.edges.map((n) => ( <BlockList>
<OAuth2Session key={n.cursor} session={n.node} /> <Title>List of OAuth 2.0 sessions:</Title>
))} {data.edges.map((n) => (
</BlockList> <OAuth2Session key={n.cursor} session={n.node} />
); ))}
</BlockList>
);
} else {
return <>Failed to load OAuth 2.0 session list</>;
}
}; };
export default OAuth2SessionList; export default OAuth2SessionList;

View File

@@ -83,7 +83,7 @@ const currentPagination = atomWithDefault<Pagination>((get) => ({
after: null, after: null,
})); }));
const emailPageResultFamily = atomFamily((userId: string) => { export const emailPageResultFamily = atomFamily((userId: string) => {
const emailPageResult = atomWithQuery({ const emailPageResult = atomWithQuery({
query: QUERY, query: QUERY,
getVariables: (get) => ({ userId, ...get(currentPagination) }), getVariables: (get) => ({ userId, ...get(currentPagination) }),

View File

@@ -0,0 +1,49 @@
// 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 { graphql } from "../gql";
import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { useAtomValue } from "jotai";
import { Title } from "./Typography";
const QUERY = graphql(/* GraphQL */ `
query UserGreeting($userId: ID!) {
user(id: $userId) {
id
username
}
}
`);
const userGreetingFamily = atomFamily((userId: string) => {
const userGreeting = atomWithQuery({
query: QUERY,
getVariables: () => ({ userId }),
});
return userGreeting;
});
const UserGreeting: React.FC<{ userId: string }> = ({ userId }) => {
const result = useAtomValue(userGreetingFamily(userId));
if (result.data?.user) {
return <Title>Hello, {result.data.user.username}!</Title>;
}
return <>Failed to load user</>;
};
export default UserGreeting;

View File

@@ -13,34 +13,32 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/
* Therefore it is highly recommended to use the babel or swc plugin for production. * Therefore it is highly recommended to use the babel or swc plugin for production.
*/ */
const documents = { const documents = {
"\n query CurrentViewerQuery {\n viewer {\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n": "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n":
types.CurrentViewerQueryDocument, types.CurrentViewerQueryDocument,
"\n query CurrentViewerSessionQuery {\n viewerSession {\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n": "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n":
types.CurrentViewerSessionQueryDocument, types.CurrentViewerSessionQueryDocument,
"\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n user {\n id\n }\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n user {\n id\n }\n email {\n id\n ...UserEmail_email\n }\n }\n }\n":
types.AddEmailDocument, types.AddEmailDocument,
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
types.BrowserSession_SessionFragmentDoc, types.BrowserSession_SessionFragmentDoc,
"\n fragment BrowserSessionList_user on User {\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n": "\n query BrowserSessionList($userId: ID!) {\n user(id: $userId) {\n id\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n }\n":
types.BrowserSessionList_UserFragmentDoc, types.BrowserSessionListDocument,
"\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n": "\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n createdAt\n deviceId\n finishedAt\n }\n }\n":
types.CompatSsoLogin_LoginFragmentDoc, types.CompatSsoLogin_LoginFragmentDoc,
"\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n": "\n query CompatSsoLoginList($userId: ID!) {\n user(id: $userId) {\n id\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n }\n":
types.CompatSsoLoginList_UserFragmentDoc, types.CompatSsoLoginListDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n": "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n":
types.OAuth2Session_SessionFragmentDoc, types.OAuth2Session_SessionFragmentDoc,
"\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n": "\n query OAuth2SessionListQuery($userId: ID!) {\n user(id: $userId) {\n id\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n }\n":
types.OAuth2SessionList_UserFragmentDoc, types.OAuth2SessionListQueryDocument,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n": "\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n":
types.UserEmail_EmailFragmentDoc, types.UserEmail_EmailFragmentDoc,
"\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": "\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n":
types.UserEmailListQueryDocument, types.UserEmailListQueryDocument,
"\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n": "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n }\n }\n":
types.AccountQueryDocument, types.UserGreetingDocument,
"\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": "\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, types.BrowserSessionQueryDocument,
"\n query HomeQuery {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_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": "\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, types.OAuth2ClientQueryDocument,
}; };
@@ -63,14 +61,14 @@ export function graphql(source: string): unknown;
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query CurrentViewerQuery {\n viewer {\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n" source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"
): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"]; ): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n" source: "\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"
): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"]; ): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n\n ... on Anonymous {\n id\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -87,8 +85,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n fragment BrowserSessionList_user on User {\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n" source: "\n query BrowserSessionList($userId: ID!) {\n user(id: $userId) {\n id\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n }\n"
): (typeof documents)["\n fragment BrowserSessionList_user on User {\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n"]; ): (typeof documents)["\n query BrowserSessionList($userId: ID!) {\n user(id: $userId) {\n id\n browserSessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -99,8 +97,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n" source: "\n query CompatSsoLoginList($userId: ID!) {\n user(id: $userId) {\n id\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n }\n"
): (typeof documents)["\n fragment CompatSsoLoginList_user on User {\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n"]; ): (typeof documents)["\n query CompatSsoLoginList($userId: ID!) {\n user(id: $userId) {\n id\n compatSsoLogins(first: 10) {\n edges {\n node {\n id\n ...CompatSsoLogin_login\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -111,8 +109,8 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n" source: "\n query OAuth2SessionListQuery($userId: ID!) {\n user(id: $userId) {\n id\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n }\n"
): (typeof documents)["\n fragment OAuth2SessionList_user on User {\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n"]; ): (typeof documents)["\n query OAuth2SessionListQuery($userId: ID!) {\n user(id: $userId) {\n id\n oauth2Sessions(first: 10) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
@@ -129,20 +127,14 @@ export function graphql(
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
export function graphql( export function graphql(
source: "\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n" source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n }\n }\n"
): (typeof documents)["\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n"]; ): (typeof documents)["\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */
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" 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"]; ): (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 # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"
): (typeof documents)["\n query HomeQuery {\n # eslint-disable-next-line @graphql-eslint/no-deprecated\n currentBrowserSession {\n id\n user {\n id\n username\n\n ...CompatSsoLoginList_user\n ...BrowserSessionList_user\n ...OAuth2SessionList_user\n }\n }\n }\n"];
/** /**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/ */

File diff suppressed because it is too large Load Diff

View File

@@ -43,8 +43,6 @@ const cache = cacheExchange({
export const client = createClient({ export const client = createClient({
url: "/graphql", url: "/graphql",
// XXX: else queries don't refetch on cache invalidation for some reason
requestPolicy: "cache-and-network",
exchanges: import.meta.env.DEV exchanges: import.meta.env.DEV
? [devtoolsExchange, cache, fetchExchange] ? [devtoolsExchange, cache, fetchExchange]
: [cache, fetchExchange], : [cache, fetchExchange],

View File

@@ -22,26 +22,12 @@ import UserEmailList from "../components/UserEmailList";
import { Title } from "../components/Typography"; import { Title } from "../components/Typography";
import AddEmailForm from "../components/AddEmailForm"; import AddEmailForm from "../components/AddEmailForm";
import { currentUserIdAtom } from "../atoms"; import { currentUserIdAtom } from "../atoms";
import UserGreeting from "../components/UserGreeting";
const QUERY = graphql(/* GraphQL */ `
query AccountQuery($id: ID!) {
user(id: $id) {
id
username
}
}
`);
const accountAtomFamily = atomFamily((id: string) =>
atomWithQuery({ query: QUERY, getVariables: () => ({ id }) })
);
const UserAccount: React.FC<{ id: string }> = ({ id }) => { const UserAccount: React.FC<{ id: string }> = ({ id }) => {
const result = useAtomValue(accountAtomFamily(id));
return ( return (
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
<Title>Hello {result.data?.user?.username}</Title> <UserGreeting userId={id} />
<UserEmailList userId={id} /> <UserEmailList userId={id} />
<AddEmailForm userId={id} /> <AddEmailForm userId={id} />
</div> </div>

View File

@@ -16,6 +16,7 @@ import { useAtomValue } from "jotai";
import { atomWithQuery } from "jotai-urql"; import { atomWithQuery } from "jotai-urql";
import { useMemo } from "react"; import { useMemo } from "react";
import { graphql } from "../gql"; import { graphql } from "../gql";
import { atomFamily } from "jotai/utils";
const QUERY = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
query BrowserSessionQuery($id: ID!) { query BrowserSessionQuery($id: ID!) {
@@ -34,25 +35,27 @@ const QUERY = graphql(/* GraphQL */ `
} }
`); `);
const BrowserSession: React.FC<{ id: string }> = ({ id }) => { const browserSessionFamily = atomFamily((id: string) => {
const result = useAtomValue( const browserSessionAtom = atomWithQuery({
useMemo( query: QUERY,
() => atomWithQuery({ query: QUERY, getVariables: () => ({ id }) }), getVariables: () => ({ id }),
[id] });
)
);
if (result.error) { return browserSessionAtom;
throw result.error; });
const BrowserSession: React.FC<{ id: string }> = ({ id }) => {
const result = useAtomValue(browserSessionFamily(id));
if (result.data?.browserSession) {
return (
<pre>
<code>{JSON.stringify(result.data.browserSession, null, 2)}</code>
</pre>
);
} }
const data = result.data!!; return <>Failed to load browser session</>;
return (
<pre>
<code>{JSON.stringify(data.browserSession, null, 2)}</code>
</pre>
);
}; };
export default BrowserSession; export default BrowserSession;

View File

@@ -20,48 +20,20 @@ import CompatSsoLoginList from "../components/CompatSsoLoginList";
import OAuth2SessionList from "../components/OAuth2SessionList"; import OAuth2SessionList from "../components/OAuth2SessionList";
import Typography from "../components/Typography"; import Typography from "../components/Typography";
import { graphql } from "../gql"; import { graphql } from "../gql";
import { currentUserIdAtom } from "../atoms";
const QUERY = graphql(/* GraphQL */ ` import UserGreeting from "../components/UserGreeting";
query HomeQuery {
# eslint-disable-next-line @graphql-eslint/no-deprecated
currentBrowserSession {
id
user {
id
username
...CompatSsoLoginList_user
...BrowserSessionList_user
...OAuth2SessionList_user
}
}
}
`);
const homeDataAtom = atomWithQuery({
query: QUERY,
});
const Home: React.FC = () => { const Home: React.FC = () => {
const result = useAtomValue(homeDataAtom); const currentUserId = useAtomValue(currentUserIdAtom);
if (result.error) {
throw result.error;
}
const data = result.data!!;
if (data.currentBrowserSession) {
const session = data.currentBrowserSession;
const user = session.user;
if (currentUserId) {
return ( return (
<> <>
<Typography variant="headline">Hello {user.username}!</Typography> <UserGreeting userId={currentUserId} />
<div className="mt-4 grid lg:grid-cols-3 gap-1"> <div className="mt-4 grid lg:grid-cols-3 gap-1">
<OAuth2SessionList user={user} /> <OAuth2SessionList userId={currentUserId} />
<CompatSsoLoginList user={user} /> <CompatSsoLoginList userId={currentUserId} />
<BrowserSessionList user={user} currentSessionId={session.id} /> <BrowserSessionList userId={currentUserId} />
</div> </div>
</> </>
); );

View File

@@ -16,6 +16,7 @@ import { useAtomValue } from "jotai";
import { useMemo } from "react"; import { useMemo } from "react";
import { atomWithQuery } from "jotai-urql"; import { atomWithQuery } from "jotai-urql";
import { graphql } from "../gql"; import { graphql } from "../gql";
import { atomFamily } from "jotai/utils";
const QUERY = graphql(/* GraphQL */ ` const QUERY = graphql(/* GraphQL */ `
query OAuth2ClientQuery($id: ID!) { query OAuth2ClientQuery($id: ID!) {
@@ -31,25 +32,27 @@ const QUERY = graphql(/* GraphQL */ `
} }
`); `);
const OAuth2Client: React.FC<{ id: string }> = ({ id }) => { const oauth2ClientFamily = atomFamily((id: string) => {
const result = useAtomValue( const oauth2ClientAtom = atomWithQuery({
useMemo( query: QUERY,
() => atomWithQuery({ query: QUERY, getVariables: () => ({ id }) }), getVariables: () => ({ id }),
[id] });
)
);
if (result.error) { return oauth2ClientAtom;
throw result.error; });
const OAuth2Client: React.FC<{ id: string }> = ({ id }) => {
const result = useAtomValue(oauth2ClientFamily(id));
if (result.data?.oauth2Client) {
return (
<pre>
<code>{JSON.stringify(result.data.oauth2Client, null, 2)}</code>
</pre>
);
} }
const data = result.data!!; return <>Failed to load OAuth2 client</>;
return (
<pre>
<code>{JSON.stringify(data.oauth2Client, null, 2)}</code>
</pre>
);
}; };
export default OAuth2Client; export default OAuth2Client;