You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-23 11:02:35 +03:00
WIP: start replacing jotai-urql with urql
This commit is contained in:
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
@@ -34,7 +34,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.0.5",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"urql": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
@@ -23549,6 +23550,18 @@
|
||||
"integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/urql": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/urql/-/urql-4.0.6.tgz",
|
||||
"integrity": "sha512-meXJ2puOd64uCGKh7Fse2R7gPa8+ZpBOoA62jN7CPXXUt7SVZSdeXWSpB3HvlfzLUkEqsWbvshwrgeWRYNNGaQ==",
|
||||
"dependencies": {
|
||||
"@urql/core": "^4.2.0",
|
||||
"wonka": "^6.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-callback-ref": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.1.tgz",
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^14.0.5",
|
||||
"ua-parser-js": "^1.0.37"
|
||||
"ua-parser-js": "^1.0.37",
|
||||
"urql": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
import { AnyVariables, CombinedError, OperationContext } from "@urql/core";
|
||||
import { atom, WritableAtom } from "jotai";
|
||||
import { useHydrateAtoms } from "jotai/utils";
|
||||
import { AtomWithQuery, atomWithQuery, clientAtom } from "jotai-urql";
|
||||
import { AtomWithQuery, clientAtom } from "jotai-urql";
|
||||
import type { ReactElement } from "react";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { graphql } from "./gql";
|
||||
import { client } from "./graphql";
|
||||
@@ -73,51 +74,15 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ `
|
||||
... on User {
|
||||
id
|
||||
}
|
||||
|
||||
... on Anonymous {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const currentViewerAtom = atomWithQuery({ query: CURRENT_VIEWER_QUERY });
|
||||
|
||||
export const currentUserIdAtom: GqlAtom<string | null> = mapQueryAtom(
|
||||
currentViewerAtom,
|
||||
(data) => {
|
||||
if (data.viewer.__typename === "User") {
|
||||
return data.viewer.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerSessionQuery {
|
||||
viewerSession {
|
||||
__typename
|
||||
... on BrowserSession {
|
||||
id
|
||||
}
|
||||
|
||||
... on Anonymous {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
const currentViewerSessionAtom = atomWithQuery({
|
||||
query: CURRENT_VIEWER_SESSION_QUERY,
|
||||
});
|
||||
|
||||
export const currentBrowserSessionIdAtom: GqlAtom<string | null> = mapQueryAtom(
|
||||
currentViewerSessionAtom,
|
||||
(data) => {
|
||||
if (data.viewerSession.__typename === "BrowserSession") {
|
||||
return data.viewerSession.id;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
export const useCurrentUserId = (): string | null => {
|
||||
const [result] = useQuery({ query: CURRENT_VIEWER_QUERY });
|
||||
if (result.error) throw result.error;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
return result.data.viewer.__typename === "User"
|
||||
? result.data.viewer.id
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import { atomFamily } from "jotai/utils";
|
||||
import { atomWithMutation } from "jotai-urql";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms";
|
||||
import { FragmentType, graphql, useFragment } from "../gql";
|
||||
import {
|
||||
parseUserAgent,
|
||||
@@ -74,21 +73,12 @@ export const useEndBrowserSession = (
|
||||
): (() => Promise<void>) => {
|
||||
const endSession = useSetAtom(endBrowserSessionFamily(sessionId));
|
||||
|
||||
// Pull those atoms to reset them when the current session is ended
|
||||
const currentUserId = useSetAtom(currentUserIdAtom);
|
||||
const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom);
|
||||
|
||||
const onSessionEnd = useCallback(async (): Promise<void> => {
|
||||
await endSession();
|
||||
if (isCurrent) {
|
||||
currentBrowserSessionId({
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
currentUserId({
|
||||
requestPolicy: "network-only",
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
}, [isCurrent, endSession, currentBrowserSessionId, currentUserId]);
|
||||
}, [isCurrent, endSession]);
|
||||
|
||||
return onSessionEnd;
|
||||
};
|
||||
|
||||
@@ -15,28 +15,17 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, afterAll, beforeEach } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { currentUserIdAtom, GqlResult } from "../../atoms";
|
||||
import { WithLocation } from "../../test-utils/WithLocation";
|
||||
|
||||
import Layout from "./Layout";
|
||||
|
||||
describe("<Layout />", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(currentUserIdAtom, "read").mockResolvedValue(
|
||||
"abc123" as unknown as GqlResult<string | null>,
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders app navigation correctly", async () => {
|
||||
const component = render(
|
||||
<WithLocation path="/">
|
||||
<Layout />
|
||||
<Layout userId="abc123" />
|
||||
</WithLocation>,
|
||||
);
|
||||
|
||||
|
||||
@@ -15,37 +15,25 @@
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { currentUserIdAtom } from "../../atoms";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../../result";
|
||||
import { appConfigAtom, routeAtom } from "../../routing";
|
||||
import Footer from "../Footer";
|
||||
import GraphQLError from "../GraphQLError";
|
||||
import NavBar from "../NavBar";
|
||||
import NavItem from "../NavItem";
|
||||
import NotLoggedIn from "../NotLoggedIn";
|
||||
import UserGreeting from "../UserGreeting";
|
||||
|
||||
import styles from "./Layout.module.css";
|
||||
|
||||
const Layout: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const Layout: React.FC<{
|
||||
userId: string;
|
||||
children?: React.ReactNode;
|
||||
}> = ({ userId, children }) => {
|
||||
const route = useAtomValue(routeAtom);
|
||||
const appConfig = useAtomValue(appConfigAtom);
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
// Hide the nav bar & user greeting on the verify-email page
|
||||
const shouldHideNavBar = route.type === "verify-email";
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null)
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<NotLoggedIn />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.layoutContainer}>
|
||||
{shouldHideNavBar ? null : (
|
||||
|
||||
@@ -13,10 +13,8 @@ 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 __typename\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 }\n":
|
||||
types.CurrentViewerQueryDocument,
|
||||
"\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 fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent\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":
|
||||
@@ -79,6 +77,8 @@ const documents = {
|
||||
types.SessionsOverviewQueryDocument,
|
||||
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n":
|
||||
types.VerifyEmailQueryDocument,
|
||||
"\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n":
|
||||
types.CurrentViewerSessionQueryDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,14 +99,8 @@ 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 __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 __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"];
|
||||
source: "\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\n id\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on User {\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.
|
||||
*/
|
||||
@@ -293,6 +287,12 @@ export function graphql(
|
||||
export function graphql(
|
||||
source: "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n",
|
||||
): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\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 __typename\n ... on BrowserSession {\n id\n }\n }\n }\n",
|
||||
): (typeof documents)["\n query CurrentViewerSessionQuery {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n }\n }\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
@@ -1111,21 +1111,7 @@ export type CurrentViewerQueryQueryVariables = Exact<{ [key: string]: never }>;
|
||||
|
||||
export type CurrentViewerQueryQuery = {
|
||||
__typename?: "Query";
|
||||
viewer:
|
||||
| { __typename: "Anonymous"; id: string }
|
||||
| { __typename: "User"; id: string };
|
||||
};
|
||||
|
||||
export type CurrentViewerSessionQueryQueryVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type CurrentViewerSessionQueryQuery = {
|
||||
__typename?: "Query";
|
||||
viewerSession:
|
||||
| { __typename: "Anonymous"; id: string }
|
||||
| { __typename: "BrowserSession"; id: string }
|
||||
| { __typename: "Oauth2Session" };
|
||||
viewer: { __typename: "Anonymous" } | { __typename: "User"; id: string };
|
||||
};
|
||||
|
||||
export type BrowserSession_SessionFragment = {
|
||||
@@ -1680,6 +1666,18 @@ export type VerifyEmailQueryQuery = {
|
||||
| null;
|
||||
};
|
||||
|
||||
export type CurrentViewerSessionQueryQueryVariables = Exact<{
|
||||
[key: string]: never;
|
||||
}>;
|
||||
|
||||
export type CurrentViewerSessionQueryQuery = {
|
||||
__typename?: "Query";
|
||||
viewerSession:
|
||||
| { __typename: "Anonymous" }
|
||||
| { __typename: "BrowserSession"; id: string }
|
||||
| { __typename: "Oauth2Session" };
|
||||
};
|
||||
|
||||
export const BrowserSession_SessionFragmentDoc = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -2090,19 +2088,6 @@ export const CurrentViewerQueryDocument = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "InlineFragment",
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "Anonymous" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -2114,60 +2099,6 @@ export const CurrentViewerQueryDocument = {
|
||||
CurrentViewerQueryQuery,
|
||||
CurrentViewerQueryQueryVariables
|
||||
>;
|
||||
export const CurrentViewerSessionQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "query",
|
||||
name: { kind: "Name", value: "CurrentViewerSessionQuery" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "viewerSession" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
|
||||
{
|
||||
kind: "InlineFragment",
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "BrowserSession" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: "InlineFragment",
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "Anonymous" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
CurrentViewerSessionQueryQuery,
|
||||
CurrentViewerSessionQueryQueryVariables
|
||||
>;
|
||||
export const EndBrowserSessionDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
@@ -4435,3 +4366,44 @@ export const VerifyEmailQueryDocument = {
|
||||
VerifyEmailQueryQuery,
|
||||
VerifyEmailQueryQueryVariables
|
||||
>;
|
||||
export const CurrentViewerSessionQueryDocument = {
|
||||
kind: "Document",
|
||||
definitions: [
|
||||
{
|
||||
kind: "OperationDefinition",
|
||||
operation: "query",
|
||||
name: { kind: "Name", value: "CurrentViewerSessionQuery" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{
|
||||
kind: "Field",
|
||||
name: { kind: "Name", value: "viewerSession" },
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "__typename" } },
|
||||
{
|
||||
kind: "InlineFragment",
|
||||
typeCondition: {
|
||||
kind: "NamedType",
|
||||
name: { kind: "Name", value: "BrowserSession" },
|
||||
},
|
||||
selectionSet: {
|
||||
kind: "SelectionSet",
|
||||
selections: [
|
||||
{ kind: "Field", name: { kind: "Name", value: "id" } },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
} as unknown as DocumentNode<
|
||||
CurrentViewerSessionQueryQuery,
|
||||
CurrentViewerSessionQueryQueryVariables
|
||||
>;
|
||||
|
||||
@@ -132,6 +132,7 @@ const exchanges = [
|
||||
|
||||
export const client = createClient({
|
||||
url: appConfig.graphqlEndpoint,
|
||||
suspense: true,
|
||||
// Add the devtools exchange in development
|
||||
exchanges: import.meta.env.DEV ? [devtoolsExchange, ...exchanges] : exchanges,
|
||||
});
|
||||
|
||||
@@ -18,32 +18,49 @@ import { DevTools } from "jotai-devtools";
|
||||
import { Suspense, StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { Provider as UrqlProvider } from "urql";
|
||||
|
||||
import { HydrateAtoms } from "./atoms";
|
||||
import { HydrateAtoms, useCurrentUserId } from "./atoms";
|
||||
import ErrorBoundary from "./components/ErrorBoundary";
|
||||
import Layout from "./components/Layout";
|
||||
import LoadingScreen from "./components/LoadingScreen";
|
||||
import LoadingSpinner from "./components/LoadingSpinner";
|
||||
import NotLoggedIn from "./components/NotLoggedIn";
|
||||
import { client } from "./graphql";
|
||||
import i18n from "./i18n";
|
||||
import { Router } from "./routing";
|
||||
import "./main.css";
|
||||
|
||||
const App: React.FC = () => {
|
||||
const userId = useCurrentUserId();
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
<Layout userId={userId}>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Router userId={userId} />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<StrictMode>
|
||||
<Provider>
|
||||
{import.meta.env.DEV && <DevTools />}
|
||||
<HydrateAtoms>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<Layout>
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Router />
|
||||
</Suspense>
|
||||
</Layout>
|
||||
</TooltipProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</HydrateAtoms>
|
||||
</Provider>
|
||||
<ErrorBoundary>
|
||||
<UrqlProvider value={client}>
|
||||
<Provider>
|
||||
{import.meta.env.DEV && <DevTools />}
|
||||
<HydrateAtoms>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<TooltipProvider>
|
||||
<App />
|
||||
</TooltipProvider>
|
||||
</I18nextProvider>
|
||||
</Suspense>
|
||||
</HydrateAtoms>
|
||||
</Provider>
|
||||
</UrqlProvider>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -12,17 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import BrowserSessionDetail from "../components/SessionDetail/BrowserSessionDetail";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query BrowserSessionQuery($id: ID!) {
|
||||
@@ -33,25 +29,15 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const browserSessionFamily = atomFamily((id: string) => {
|
||||
const browserSessionQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: () => ({ id }),
|
||||
});
|
||||
|
||||
const browserSessionAtom = mapQueryAtom(
|
||||
browserSessionQueryAtom,
|
||||
(data) => data?.browserSession,
|
||||
);
|
||||
|
||||
return browserSessionAtom;
|
||||
});
|
||||
|
||||
const BrowserSession: React.FC<{ id: string }> = ({ id }) => {
|
||||
const result = useAtomValue(browserSessionFamily(id));
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
const [result] = useQuery({
|
||||
query: QUERY,
|
||||
variables: { id },
|
||||
});
|
||||
if (result.error) return <GraphQLError error={result.error} />;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
|
||||
const browserSession = unwrapOk(result);
|
||||
const browserSession = result.data.browserSession;
|
||||
if (!browserSession) return <NotFound />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,27 +12,10 @@
|
||||
// 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 ErrorBoundary from "../components/ErrorBoundary";
|
||||
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 (
|
||||
<ErrorBoundary>
|
||||
<List userId={userId} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
return <List userId={userId} />;
|
||||
};
|
||||
|
||||
export default BrowserSessionList;
|
||||
|
||||
@@ -12,17 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotFound from "../components/NotFound";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query OAuth2ClientQuery($id: ID!) {
|
||||
@@ -32,25 +28,15 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const oauth2ClientFamily = atomFamily((id: string) => {
|
||||
const oauth2ClientQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: () => ({ id }),
|
||||
});
|
||||
|
||||
const oauth2ClientAtom = mapQueryAtom(
|
||||
oauth2ClientQueryAtom,
|
||||
(data) => data?.oauth2Client,
|
||||
);
|
||||
|
||||
return oauth2ClientAtom;
|
||||
});
|
||||
|
||||
const OAuth2Client: React.FC<{ id: string }> = ({ id }) => {
|
||||
const result = useAtomValue(oauth2ClientFamily(id));
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
const [result] = useQuery({
|
||||
query: QUERY,
|
||||
variables: { id },
|
||||
});
|
||||
if (result.error) return <GraphQLError error={result.error} />;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
|
||||
const oauth2Client = unwrapOk(result);
|
||||
const oauth2Client = result.data.oauth2Client;
|
||||
if (!oauth2Client) return <NotFound />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,22 +12,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import UserProfile from "../components/UserProfile";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
const Profile: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<UserProfile userId={userId} />
|
||||
|
||||
@@ -12,27 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
import { currentUserIdAtom } from "../atoms";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import UserSessionDetail from "../components/SessionDetail";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const SessionDetail: React.FC<{ deviceId: string }> = ({ deviceId }) => {
|
||||
const result = useAtomValue(currentUserIdAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
|
||||
const userId = unwrapOk(result);
|
||||
if (userId === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<UserSessionDetail userId={userId} deviceId={deviceId} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
const SessionDetail: React.FC<{ userId: string; deviceId: string }> = ({
|
||||
userId,
|
||||
deviceId,
|
||||
}) => {
|
||||
return <UserSessionDetail userId={userId} deviceId={deviceId} />;
|
||||
};
|
||||
|
||||
export default SessionDetail;
|
||||
|
||||
@@ -12,16 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import NotLoggedIn from "../components/NotLoggedIn";
|
||||
import UserSessionsOverview from "../components/UserSessionsOverview";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query SessionsOverviewQuery {
|
||||
@@ -36,23 +33,13 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const sessionsOverviewQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
});
|
||||
|
||||
const sessionsOverviewAtom = mapQueryAtom(sessionsOverviewQueryAtom, (data) => {
|
||||
if (data.viewer?.__typename === "User") {
|
||||
return data.viewer;
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const SessionsOverview: React.FC = () => {
|
||||
const result = useAtomValue(sessionsOverviewAtom);
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
const [result] = useQuery({ query: QUERY });
|
||||
if (result.error) return <GraphQLError error={result.error} />;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
|
||||
const data = unwrapOk(result);
|
||||
const data =
|
||||
result.data.viewer.__typename === "User" ? result.data.viewer : null;
|
||||
if (data === null) return <NotLoggedIn />;
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,17 +12,13 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { useAtomValue } from "jotai";
|
||||
import { atomFamily } from "jotai/utils";
|
||||
import { atomWithQuery } from "jotai-urql";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { mapQueryAtom } from "../atoms";
|
||||
import ErrorBoundary from "../components/ErrorBoundary";
|
||||
import GraphQLError from "../components/GraphQLError";
|
||||
import VerifyEmailComponent from "../components/VerifyEmail";
|
||||
import { graphql } from "../gql";
|
||||
import { isErr, unwrapErr, unwrapOk } from "../result";
|
||||
|
||||
const QUERY = graphql(/* GraphQL */ `
|
||||
query VerifyEmailQuery($id: ID!) {
|
||||
@@ -32,27 +28,14 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
`);
|
||||
|
||||
const verifyEmailFamily = atomFamily((id: string) => {
|
||||
const verifyEmailQueryAtom = atomWithQuery({
|
||||
query: QUERY,
|
||||
getVariables: () => ({ id }),
|
||||
});
|
||||
|
||||
const verifyEmailAtom = mapQueryAtom(
|
||||
verifyEmailQueryAtom,
|
||||
(data) => data?.userEmail,
|
||||
);
|
||||
|
||||
return verifyEmailAtom;
|
||||
});
|
||||
|
||||
const VerifyEmail: React.FC<{ id: string }> = ({ id }) => {
|
||||
const result = useAtomValue(verifyEmailFamily(id));
|
||||
const [result] = useQuery({ query: QUERY, variables: { id } });
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
|
||||
if (result.error) return <GraphQLError error={result.error} />;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
|
||||
const email = unwrapOk(result);
|
||||
const email = result.data.userEmail;
|
||||
if (email == null) return <>{t("frontend.verify_email.unknown_email")}</>;
|
||||
|
||||
return (
|
||||
|
||||
@@ -55,7 +55,7 @@ const unknownRoute = (route: never): never => {
|
||||
throw new Error(`Invalid route: ${JSON.stringify(route)}`);
|
||||
};
|
||||
|
||||
const Router: React.FC = () => {
|
||||
const Router: React.FC<{ userId: string }> = ({ userId }) => {
|
||||
const [route, redirecting] = useRouteWithRedirect();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -65,13 +65,13 @@ const Router: React.FC = () => {
|
||||
|
||||
switch (route.type) {
|
||||
case "profile":
|
||||
return <Profile />;
|
||||
return <Profile userId={userId} />;
|
||||
case "sessions-overview":
|
||||
return <SessionsOverview />;
|
||||
case "session":
|
||||
return <SessionDetail deviceId={route.id} />;
|
||||
return <SessionDetail userId={userId} deviceId={route.id} />;
|
||||
case "browser-session-list":
|
||||
return <BrowserSessionList />;
|
||||
return <BrowserSessionList userId={userId} />;
|
||||
case "client":
|
||||
return <OAuth2Client id={route.id} />;
|
||||
case "browser-session":
|
||||
|
||||
@@ -12,11 +12,20 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { CombinedError } from "@urql/core";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useQuery } from "urql";
|
||||
|
||||
import { currentBrowserSessionIdAtom } from "../../atoms";
|
||||
import { isOk, unwrapOk, unwrapErr, isErr } from "../../result";
|
||||
import { graphql } from "../../gql";
|
||||
|
||||
const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ `
|
||||
query CurrentViewerSessionQuery {
|
||||
viewerSession {
|
||||
__typename
|
||||
... on BrowserSession {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
/**
|
||||
* Query the current browser session id
|
||||
@@ -24,16 +33,10 @@ import { isOk, unwrapOk, unwrapErr, isErr } from "../../result";
|
||||
* throws error when error result
|
||||
*/
|
||||
export const useCurrentBrowserSessionId = (): string | null => {
|
||||
const currentSessionIdResult = useAtomValue(currentBrowserSessionIdAtom);
|
||||
|
||||
if (isErr(currentSessionIdResult)) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw unwrapErr<CombinedError>(currentSessionIdResult) as Error;
|
||||
}
|
||||
|
||||
if (isOk<string | null, unknown>(currentSessionIdResult)) {
|
||||
return unwrapOk<string | null>(currentSessionIdResult);
|
||||
}
|
||||
|
||||
return null;
|
||||
const [result] = useQuery({ query: CURRENT_VIEWER_SESSION_QUERY });
|
||||
if (result.error) throw result.error;
|
||||
if (!result.data) throw new Error(); // Suspense mode is enabled
|
||||
return result.data.viewer.__typename === "User"
|
||||
? result.data.viewer.id
|
||||
: null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user