1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-31 09:24:31 +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 */ `
query CurrentViewerQuery {
viewer {
__typename
... on User {
id
}
@ -52,6 +53,7 @@ export const currentUserIdAtom = atom(async (get) => {
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
query CurrentViewerSessionQuery {
viewerSession {
__typename
... on BrowserSession {
id
}

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ const currentPagination = atomWithDefault<Pagination>((get) => ({
after: null,
}));
const emailPageResultFamily = atomFamily((userId: string) => {
export const emailPageResultFamily = atomFamily((userId: string) => {
const emailPageResult = atomWithQuery({
query: QUERY,
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.
*/
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,
"\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,
"\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,
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n":
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":
types.BrowserSessionList_UserFragmentDoc,
"\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.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":
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":
types.CompatSsoLoginList_UserFragmentDoc,
"\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.CompatSsoLoginListDocument,
"\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,
"\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":
types.OAuth2SessionList_UserFragmentDoc,
"\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.OAuth2SessionListQueryDocument,
"\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n":
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":
types.UserEmailListQueryDocument,
"\n query AccountQuery($id: ID!) {\n user(id: $id) {\n id\n username\n }\n }\n":
types.AccountQueryDocument,
"\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\n id\n username\n }\n }\n":
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":
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":
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.
*/
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"
): (typeof documents)["\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 __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.
*/
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"
): (typeof documents)["\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 __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.
*/
@ -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.
*/
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"
): (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"];
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 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.
*/
@ -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.
*/
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"
): (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"];
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 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.
*/
@ -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.
*/
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"
): (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"];
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 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.
*/
@ -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.
*/
export function graphql(
source: "\n query AccountQuery($id: ID!) {\n user(id: $id) {\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"];
source: "\n query UserGreeting($userId: ID!) {\n user(id: $userId) {\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.
*/
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 # 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.
*/

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -20,48 +20,20 @@ import CompatSsoLoginList from "../components/CompatSsoLoginList";
import OAuth2SessionList from "../components/OAuth2SessionList";
import Typography from "../components/Typography";
import { graphql } from "../gql";
const QUERY = graphql(/* GraphQL */ `
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,
});
import { currentUserIdAtom } from "../atoms";
import UserGreeting from "../components/UserGreeting";
const Home: React.FC = () => {
const result = useAtomValue(homeDataAtom);
if (result.error) {
throw result.error;
}
const data = result.data!!;
if (data.currentBrowserSession) {
const session = data.currentBrowserSession;
const user = session.user;
const currentUserId = useAtomValue(currentUserIdAtom);
if (currentUserId) {
return (
<>
<Typography variant="headline">Hello {user.username}!</Typography>
<UserGreeting userId={currentUserId} />
<div className="mt-4 grid lg:grid-cols-3 gap-1">
<OAuth2SessionList user={user} />
<CompatSsoLoginList user={user} />
<BrowserSessionList user={user} currentSessionId={session.id} />
<OAuth2SessionList userId={currentUserId} />
<CompatSsoLoginList userId={currentUserId} />
<BrowserSessionList userId={currentUserId} />
</div>
</>
);

View File

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