From f67cc0d6d080c30fe34e1ee98ee82c071c8564bf Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 20 Jun 2023 15:05:48 +0200 Subject: [PATCH] Frontend cleanups Mainly: - better handling of GraphQL errors - better logout state - dependencies update - a way to end browser sessions in the GraphQL API --- .../graphql/src/mutations/browser_session.rs | 103 +++++ crates/graphql/src/mutations/mod.rs | 2 + frontend/package-lock.json | 355 ++++++++++++++---- frontend/package.json | 2 +- frontend/postcss.config.cjs | 6 +- frontend/schema.graphql | 36 ++ frontend/src/atoms.ts | 67 +++- frontend/src/components/BrowserSession.tsx | 66 +++- .../src/components/BrowserSessionList.tsx | 61 +-- .../src/components/CompatSsoLoginList.tsx | 48 ++- frontend/src/components/GraphQLError.tsx | 24 ++ frontend/src/components/NotFound.tsx | 19 + frontend/src/components/NotLoggedIn.tsx | 21 ++ frontend/src/components/OAuth2Session.tsx | 2 +- frontend/src/components/OAuth2SessionList.tsx | 48 ++- frontend/src/gql/gql.ts | 16 +- frontend/src/gql/graphql.ts | 157 +++++++- frontend/src/gql/schema.ts | 50 +++ frontend/src/pages/Account.tsx | 22 +- frontend/src/pages/BrowserSession.tsx | 27 +- frontend/src/pages/Home.tsx | 34 +- frontend/src/pages/OAuth2Client.tsx | 27 +- frontend/src/result.ts | 61 +++ frontend/vite.config.ts | 48 ++- 24 files changed, 1072 insertions(+), 230 deletions(-) create mode 100644 crates/graphql/src/mutations/browser_session.rs create mode 100644 frontend/src/components/GraphQLError.tsx create mode 100644 frontend/src/components/NotFound.tsx create mode 100644 frontend/src/components/NotLoggedIn.tsx create mode 100644 frontend/src/result.ts diff --git a/crates/graphql/src/mutations/browser_session.rs b/crates/graphql/src/mutations/browser_session.rs new file mode 100644 index 00000000..39a2f0ff --- /dev/null +++ b/crates/graphql/src/mutations/browser_session.rs @@ -0,0 +1,103 @@ +// 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. + +use anyhow::Context as _; +use async_graphql::{Context, Enum, InputObject, Object, ID}; +use mas_storage::RepositoryAccess; + +use crate::{ + model::{BrowserSession, NodeType}, + state::ContextExt, +}; + +#[derive(Default)] +pub struct BrowserSessionMutations { + _private: (), +} + +/// The input of the `endBrowserSession` mutation. +#[derive(InputObject)] +pub struct EndBrowserSessionInput { + /// The ID of the session to end. + browser_session_id: ID, +} + +/// The payload of the `endBrowserSession` mutation. +pub enum EndBrowserSessionPayload { + NotFound, + Ended(mas_data_model::BrowserSession), +} + +/// The status of the `endBrowserSession` mutation. +#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)] +enum EndBrowserSessionStatus { + /// The session was ended. + Ended, + + /// The session was not found. + NotFound, +} + +#[Object] +impl EndBrowserSessionPayload { + /// The status of the mutation. + async fn status(&self) -> EndBrowserSessionStatus { + match self { + Self::Ended(_) => EndBrowserSessionStatus::Ended, + Self::NotFound => EndBrowserSessionStatus::NotFound, + } + } + + /// Returns the ended session. + async fn browser_session(&self) -> Option { + match self { + Self::Ended(session) => Some(BrowserSession(session.clone())), + Self::NotFound => None, + } + } +} + +#[Object] +impl BrowserSessionMutations { + async fn end_browser_session( + &self, + ctx: &Context<'_>, + input: EndBrowserSessionInput, + ) -> Result { + let state = ctx.state(); + let browser_session_id = + NodeType::BrowserSession.extract_ulid(&input.browser_session_id)?; + let requester = ctx.requester(); + + let user = requester.user().context("Unauthorized")?; + + let mut repo = state.repository().await?; + let clock = state.clock(); + + let session = repo.browser_session().lookup(browser_session_id).await?; + let Some(session) = session else { + return Ok(EndBrowserSessionPayload::NotFound); + }; + + if session.user.id != user.id { + return Err(async_graphql::Error::new("Unauthorized")); + } + + let session = repo.browser_session().finish(&clock, session).await?; + + repo.save().await?; + + Ok(EndBrowserSessionPayload::Ended(session)) + } +} diff --git a/crates/graphql/src/mutations/mod.rs b/crates/graphql/src/mutations/mod.rs index c1b5d3c0..7ac04425 100644 --- a/crates/graphql/src/mutations/mod.rs +++ b/crates/graphql/src/mutations/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod browser_session; mod compat_session; mod oauth2_session; mod user_email; @@ -24,6 +25,7 @@ pub struct Mutation( user_email::UserEmailMutations, oauth2_session::OAuth2SessionMutations, compat_session::CompatSessionMutations, + browser_session::BrowserSessionMutations, ); impl Mutation { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 22d93aa1..5b51a937 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,7 +22,8 @@ "jotai-location": "^0.5.1", "jotai-urql": "^0.7.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "vite-plugin-svgr": "^3.2.0" }, "devDependencies": { "@graphql-codegen/cli": "^4.0.1", @@ -95,7 +96,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -344,7 +344,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -353,7 +352,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", - "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -418,7 +416,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5", "@jridgewell/gen-mapping": "^0.3.2", @@ -457,7 +454,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", - "dev": true, "dependencies": { "@babel/compat-data": "^7.22.5", "@babel/helper-validator-option": "^7.22.5", @@ -533,7 +529,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -542,7 +537,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", - "dev": true, "dependencies": { "@babel/template": "^7.22.5", "@babel/types": "^7.22.5" @@ -555,7 +549,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -590,7 +583,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", - "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -665,7 +657,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -689,7 +680,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", - "dev": true, "dependencies": { "@babel/types": "^7.22.5" }, @@ -717,7 +707,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -741,7 +730,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", - "dev": true, "dependencies": { "@babel/template": "^7.22.5", "@babel/traverse": "^7.22.5", @@ -768,7 +756,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -2610,7 +2597,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.22.5", "@babel/parser": "^7.22.5", @@ -2624,7 +2610,6 @@ "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", - "dev": true, "dependencies": { "@babel/code-frame": "^7.22.5", "@babel/generator": "^7.22.5", @@ -2795,7 +2780,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -2811,7 +2795,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -2827,7 +2810,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -2843,7 +2825,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -2859,7 +2840,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -2875,7 +2855,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2891,7 +2870,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -2907,7 +2885,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2923,7 +2900,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2939,7 +2915,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2955,7 +2930,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2971,7 +2945,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -2987,7 +2960,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3003,7 +2975,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3019,7 +2990,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3035,7 +3005,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -3051,7 +3020,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -3067,7 +3035,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -3083,7 +3050,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -3099,7 +3065,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -3115,7 +3080,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -3131,7 +3095,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -5170,7 +5133,6 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -5184,7 +5146,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -5193,7 +5154,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, "engines": { "node": ">=6.0.0" } @@ -5201,14 +5161,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.18", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "3.1.0", "@jridgewell/sourcemap-codec": "1.4.14" @@ -5217,8 +5175,7 @@ "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@juggle/resize-observer": { "version": "3.4.0", @@ -7572,6 +7529,225 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-7.0.0.tgz", + "integrity": "sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-7.0.0.tgz", + "integrity": "sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-7.0.0.tgz", + "integrity": "sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-7.0.0.tgz", + "integrity": "sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-7.0.0.tgz", + "integrity": "sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-7.0.0.tgz", + "integrity": "sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-7.0.0.tgz", + "integrity": "sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-7.0.0.tgz", + "integrity": "sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-7.0.0.tgz", + "integrity": "sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^7.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^7.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^7.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^7.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "^7.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "^7.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "^7.0.0", + "@svgr/babel-plugin-transform-svg-component": "^7.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-7.0.0.tgz", + "integrity": "sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "^7.0.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-7.0.0.tgz", + "integrity": "sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-7.0.0.tgz", + "integrity": "sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "^7.0.0", + "@svgr/hast-util-to-babel-ast": "^7.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, "node_modules/@tabler/icons": { "version": "2.22.0", "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-2.22.0.tgz", @@ -7845,7 +8021,7 @@ "version": "20.3.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==", - "dev": true + "devOptional": true }, "node_modules/@types/node-fetch": { "version": "2.6.4", @@ -8804,8 +8980,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/aria-hidden": { "version": "1.2.3", @@ -9474,7 +9649,6 @@ "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9716,7 +9890,6 @@ "version": "1.0.30001504", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001504.tgz", "integrity": "sha512-5uo7eoOp2mKbWyfMXnGO9rJWOGU8duvzEiYITW+wivukL7yHH4gX9yuRaobu6El4jPxo6jKZfG+N6fB621GD/Q==", - "dev": true, "funding": [ { "type": "opencollective", @@ -10415,7 +10588,6 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", - "dev": true, "dependencies": { "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -10589,7 +10761,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -11132,8 +11303,7 @@ "node_modules/electron-to-chromium": { "version": "1.4.433", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.433.tgz", - "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==", - "dev": true + "integrity": "sha512-MGO1k0w1RgrfdbLVwmXcDhHHuxCn2qRgR7dYsJvWFKDttvYPx6FNzCGG0c/fBBvzK2LDh3UV7Tt9awnHnvAAUQ==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -11304,7 +11474,6 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -11359,7 +11528,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -12267,8 +12435,7 @@ "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/esutils": { "version": "2.0.3", @@ -12953,7 +13120,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -13019,7 +13185,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "engines": { "node": ">=6.9.0" } @@ -13262,7 +13427,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "engines": { "node": ">=4" } @@ -14858,7 +15022,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -15002,7 +15165,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, "bin": { "jsesc": "bin/jsesc" }, @@ -15056,7 +15218,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -15449,7 +15610,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "dependencies": { "yallist": "^3.0.2" } @@ -15801,8 +15961,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -15825,7 +15984,6 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, "funding": [ { "type": "github", @@ -15940,8 +16098,7 @@ "node_modules/node-releases": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", - "dev": true + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==" }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -16612,7 +16769,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -16687,7 +16843,6 @@ "version": "8.4.24", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -17984,7 +18139,6 @@ "version": "3.25.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", - "dev": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -18120,7 +18274,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, "bin": { "semver": "bin/semver.js" } @@ -18433,7 +18586,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -18854,6 +19006,11 @@ "resolved": "https://registry.npmjs.org/svg-arc-to-cubic-bezier/-/svg-arc-to-cubic-bezier-3.2.0.tgz", "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==" }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==" + }, "node_modules/svg-path-bounds": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/svg-path-bounds/-/svg-path-bounds-1.0.2.tgz", @@ -19742,7 +19899,6 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -19984,7 +20140,6 @@ "version": "4.3.9", "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", - "dev": true, "dependencies": { "esbuild": "^0.17.5", "postcss": "^8.4.23", @@ -20062,6 +20217,45 @@ "vite": "^2.7.0 || ^3.0.0 || ^4.0.0" } }, + "node_modules/vite-plugin-svgr": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-3.2.0.tgz", + "integrity": "sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==", + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "@svgr/core": "^7.0.0", + "@svgr/plugin-jsx": "^7.0.0" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@rollup/pluginutils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", + "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/vite-plugin-svgr/node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + }, "node_modules/vitest": { "version": "0.32.2", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.2.tgz", @@ -20461,8 +20655,7 @@ "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/yaml": { "version": "1.10.2", diff --git a/frontend/package.json b/frontend/package.json index 2328e413..e304204a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -63,7 +63,6 @@ "eslint-plugin-matrix-org": "^1.2.0", "eslint-plugin-prettier": "^4.2.1", "postcss": "^8.4.24", - "postcss-prune-var": "^1.1.1", "prettier": "2.8.0", "react-test-renderer": "^18.2.0", "storybook": "^7.0.22", @@ -71,6 +70,7 @@ "typescript": "5.1.3", "vite": "^4.3.9", "vite-plugin-graphql-codegen": "^3.2.2", + "vite-plugin-svgr": "^3.2.0", "vitest": "^0.32.2" } } diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs index 1d134efb..d6784e74 100644 --- a/frontend/postcss.config.cjs +++ b/frontend/postcss.config.cjs @@ -17,9 +17,5 @@ /** @type {import('postcss-load-config').Config} */ module.exports = { - plugins: [ - require("tailwindcss"), - require("autoprefixer"), - require("postcss-prune-var"), - ], + plugins: [require("tailwindcss"), require("autoprefixer")], }; diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 80d1259f..75a0073d 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -223,6 +223,41 @@ The input/output is a string in RFC3339 format. """ scalar DateTime +""" +The input of the `endBrowserSession` mutation. +""" +input EndBrowserSessionInput { + """ + The ID of the session to end. + """ + browserSessionId: ID! +} + +type EndBrowserSessionPayload { + """ + The status of the mutation. + """ + status: EndBrowserSessionStatus! + """ + Returns the ended session. + """ + browserSession: BrowserSession +} + +""" +The status of the `endBrowserSession` mutation. +""" +enum EndBrowserSessionStatus { + """ + The session was ended. + """ + ENDED + """ + The session was not found. + """ + NOT_FOUND +} + """ The input of the `endCompatSession` mutation. """ @@ -336,6 +371,7 @@ type Mutation { setPrimaryEmail(input: SetPrimaryEmailInput!): SetPrimaryEmailPayload! endOauth2Session(input: EndOAuth2SessionInput!): EndOAuth2SessionPayload! endCompatSession(input: EndCompatSessionInput!): EndCompatSessionPayload! + endBrowserSession(input: EndBrowserSessionInput!): EndBrowserSessionPayload! } """ diff --git a/frontend/src/atoms.ts b/frontend/src/atoms.ts index bff3b3e1..703626f4 100644 --- a/frontend/src/atoms.ts +++ b/frontend/src/atoms.ts @@ -12,13 +12,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { atom } from "jotai"; +import { AnyVariables, CombinedError, OperationContext } from "@urql/core"; +import { atom, WritableAtom } from "jotai"; import { useHydrateAtoms } from "jotai/utils"; -import { atomWithQuery, clientAtom } from "jotai-urql"; +import { AtomWithQuery, atomWithQuery, clientAtom } from "jotai-urql"; import type { ReactElement } from "react"; import { graphql } from "./gql"; import { client } from "./graphql"; +import { err, ok, Result } from "./result"; + +export type GqlResult = Result; +export type GqlAtom = WritableAtom< + Promise>, + [context?: Partial], + void +>; + +/** + * Map the result of a query atom to a new value, making it a GqlResult + * + * @param queryAtom: An atom got from atomWithQuery + * @param mapper: A function that takes the data from the query and returns a new value + */ +export const mapQueryAtom = ( + queryAtom: AtomWithQuery, + mapper: (data: Data) => NewData +): GqlAtom => { + return atom( + async (get): Promise> => { + const result = await get(queryAtom); + if (result.error) { + return err(result.error); + } + + if (result.data === undefined) { + throw new Error("Query result is undefined"); + } + + return ok(mapper(result.data)); + }, + + (_get, set, context) => { + set(queryAtom, context); + } + ); +}; export const HydrateAtoms: React.FC<{ children: ReactElement }> = ({ children, @@ -44,13 +83,15 @@ const CURRENT_VIEWER_QUERY = graphql(/* GraphQL */ ` const currentViewerAtom = atomWithQuery({ query: CURRENT_VIEWER_QUERY }); -export const currentUserIdAtom = atom(async (get) => { - const result = await get(currentViewerAtom); - if (result.data?.viewer.__typename === "User") { - return result.data.viewer.id; +export const currentUserIdAtom: GqlAtom = mapQueryAtom( + currentViewerAtom, + (data) => { + if (data.viewer.__typename === "User") { + return data.viewer.id; + } + return null; } - return null; -}); +); const CURRENT_VIEWER_SESSION_QUERY = graphql(/* GraphQL */ ` query CurrentViewerSessionQuery { @@ -71,11 +112,11 @@ const currentViewerSessionAtom = atomWithQuery({ query: CURRENT_VIEWER_SESSION_QUERY, }); -export const currentBrowserSessionIdAtom = atom( - async (get): Promise => { - const result = await get(currentViewerSessionAtom); - if (result.data?.viewerSession.__typename === "BrowserSession") { - return result.data.viewerSession.id; +export const currentBrowserSessionIdAtom: GqlAtom = mapQueryAtom( + currentViewerSessionAtom, + (data) => { + if (data.viewerSession.__typename === "BrowserSession") { + return data.viewerSession.id; } return null; } diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx index 77425e26..1fc860e2 100644 --- a/frontend/src/components/BrowserSession.tsx +++ b/frontend/src/components/BrowserSession.tsx @@ -13,8 +13,13 @@ // limitations under the License. import IconWebBrowser from "@vector-im/compound-design-tokens/icons/web-browser.svg"; -import { Body } from "@vector-im/compound-web"; +import { Body, Button } from "@vector-im/compound-web"; +import { atom, useSetAtom } from "jotai"; +import { atomFamily } from "jotai/utils"; +import { atomWithMutation } from "jotai-urql"; +import { useTransition } from "react"; +import { currentBrowserSessionIdAtom, currentUserIdAtom } from "../atoms"; import { FragmentType, graphql, useFragment } from "../gql"; import Block from "./Block"; @@ -31,6 +36,30 @@ const FRAGMENT = graphql(/* GraphQL */ ` } `); +const END_SESSION_MUTATION = graphql(/* GraphQL */ ` + mutation EndBrowserSession($id: ID!) { + endBrowserSession(input: { browserSessionId: $id }) { + status + browserSession { + id + ...BrowserSession_session + } + } + } +`); + +const endSessionFamily = atomFamily((id: string) => { + const endSession = atomWithMutation(END_SESSION_MUTATION); + + // A proxy atom which pre-sets the id variable in the mutation + const endSessionAtom = atom( + (get) => get(endSession), + (get, set) => set(endSession, { id }) + ); + + return endSessionAtom; +}); + type Props = { session: FragmentType; isCurrent: boolean; @@ -38,10 +67,30 @@ type Props = { const BrowserSession: React.FC = ({ session, isCurrent }) => { const data = useFragment(FRAGMENT, session); + const [pending, startTransition] = useTransition(); + const endSession = useSetAtom(endSessionFamily(data.id)); + + // Pull those atoms to reset them when the current session is ended + const currentUserId = useSetAtom(currentUserIdAtom); + const currentBrowserSessionId = useSetAtom(currentBrowserSessionIdAtom); - // const lastAuthentication = data.lastAuthentication?.createdAt; const createdAt = data.createdAt; + const onSessionEnd = () => { + startTransition(() => { + endSession().then(() => { + if (isCurrent) { + currentBrowserSessionId({ + requestPolicy: "network-only", + }); + currentUserId({ + requestPolicy: "network-only", + }); + } + }); + }); + }; + return ( @@ -59,15 +108,16 @@ const BrowserSession: React.FC = ({ session, isCurrent }) => { Signed in - Sign out - + ); }; diff --git a/frontend/src/components/BrowserSessionList.tsx b/frontend/src/components/BrowserSessionList.tsx index 83b66590..3f5503b5 100644 --- a/frontend/src/components/BrowserSessionList.tsx +++ b/frontend/src/components/BrowserSessionList.tsx @@ -17,7 +17,7 @@ import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useTransition } from "react"; -import { currentBrowserSessionIdAtom } from "../atoms"; +import { currentBrowserSessionIdAtom, mapQueryAtom } from "../atoms"; import { graphql } from "../gql"; import { PageInfo } from "../gql/graphql"; import { @@ -25,9 +25,11 @@ import { atomWithPagination, Pagination, } from "../pagination"; +import { isErr, isOk, unwrapErr, unwrapOk } from "../result"; import BlockList from "./BlockList"; import BrowserSession from "./BrowserSession"; +import GraphQLError from "./GraphQLError"; import PaginationControls from "./PaginationControls"; import { Title } from "./Typography"; @@ -69,17 +71,23 @@ const QUERY = graphql(/* GraphQL */ ` const currentPaginationAtom = atomForCurrentPagination(); const browserSessionListFamily = atomFamily((userId: string) => { - const browserSessionList = atomWithQuery({ + const browserSessionListQuery = atomWithQuery({ query: QUERY, getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }), }); + + const browserSessionList = mapQueryAtom( + browserSessionListQuery, + (data) => data.user?.browserSessions || null + ); + return browserSessionList; }); const pageInfoFamily = atomFamily((userId: string) => { const pageInfoAtom = atom(async (get): Promise => { const result = await get(browserSessionListFamily(userId)); - return result.data?.user?.browserSessions?.pageInfo ?? null; + return (isOk(result) && unwrapOk(result)?.pageInfo) || null; }); return pageInfoAtom; }); @@ -94,40 +102,43 @@ const paginationFamily = atomFamily((userId: string) => { }); const BrowserSessionList: React.FC<{ userId: string }> = ({ userId }) => { - const currentSessionId = useAtomValue(currentBrowserSessionIdAtom); + const currentSessionIdResult = useAtomValue(currentBrowserSessionIdAtom); const [pending, startTransition] = useTransition(); const result = useAtomValue(browserSessionListFamily(userId)); const setPagination = useSetAtom(currentPaginationAtom); const [prevPage, nextPage] = useAtomValue(paginationFamily(userId)); + if (isErr(currentSessionIdResult)) + return ; + if (isErr(result)) return ; + + const browserSessions = unwrapOk(result); + if (browserSessions === null) return <>Failed to load browser sessions; + const currentSessionId = unwrapOk(currentSessionIdResult); + const paginate = (pagination: Pagination): void => { startTransition(() => { setPagination(pagination); }); }; - if (result.data?.user?.browserSessions) { - const data = result.data.user.browserSessions; - return ( - - List of browser sessions: - paginate(prevPage) : null} - onNext={nextPage ? (): void => paginate(nextPage) : null} - disabled={pending} + return ( + + List of browser sessions: + paginate(prevPage) : null} + onNext={nextPage ? (): void => paginate(nextPage) : null} + disabled={pending} + /> + {browserSessions.edges.map((n) => ( + - {data.edges.map((n) => ( - - ))} - - ); - } - - return <>Failed to load browser sessions; + ))} + + ); }; export default BrowserSessionList; diff --git a/frontend/src/components/CompatSsoLoginList.tsx b/frontend/src/components/CompatSsoLoginList.tsx index d23d4c84..c06ced25 100644 --- a/frontend/src/components/CompatSsoLoginList.tsx +++ b/frontend/src/components/CompatSsoLoginList.tsx @@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useTransition } from "react"; +import { mapQueryAtom } from "../atoms"; import { graphql } from "../gql"; import { PageInfo } from "../gql/graphql"; import { @@ -24,9 +25,11 @@ import { atomWithPagination, Pagination, } from "../pagination"; +import { isErr, isOk, unwrapErr, unwrapOk } from "../result"; import BlockList from "./BlockList"; import CompatSsoLogin from "./CompatSsoLogin"; +import GraphQLError from "./GraphQLError"; import PaginationControls from "./PaginationControls"; import { Title } from "./Typography"; @@ -67,18 +70,23 @@ const QUERY = graphql(/* GraphQL */ ` const currentPaginationAtom = atomForCurrentPagination(); const compatSsoLoginListFamily = atomFamily((userId: string) => { - const compatSsoLoginList = atomWithQuery({ + const compatSsoLoginListQuery = atomWithQuery({ query: QUERY, getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }), }); + const compatSsoLoginList = mapQueryAtom( + compatSsoLoginListQuery, + (data) => data.user?.compatSsoLogins || null + ); + return compatSsoLoginList; }); const pageInfoFamily = atomFamily((userId: string) => { const pageInfoAtom = atom(async (get): Promise => { const result = await get(compatSsoLoginListFamily(userId)); - return result.data?.user?.compatSsoLogins?.pageInfo ?? null; + return (isOk(result) && unwrapOk(result)?.pageInfo) || null; }); return pageInfoAtom; @@ -98,30 +106,30 @@ const CompatSsoLoginList: React.FC<{ userId: string }> = ({ userId }) => { const setPagination = useSetAtom(currentPaginationAtom); const [prevPage, nextPage] = useAtomValue(paginationFamily(userId)); + if (isErr(result)) return ; + const compatSsoLoginList = unwrapOk(result); + if (compatSsoLoginList === null) + return <>Failed to load list of compatibility sessions.; + const paginate = (pagination: Pagination): void => { startTransition(() => { setPagination(pagination); }); }; - if (result.data?.user?.compatSsoLogins) { - const data = result.data.user.compatSsoLogins; - return ( - - List of compatibility sessions: - paginate(prevPage) : null} - onNext={nextPage ? (): void => paginate(nextPage) : null} - disabled={pending} - /> - {data.edges.map((n) => ( - - ))} - - ); - } - - return <>Failed to load list of compatibility sessions.; + return ( + + List of compatibility sessions: + paginate(prevPage) : null} + onNext={nextPage ? (): void => paginate(nextPage) : null} + disabled={pending} + /> + {compatSsoLoginList.edges.map((n) => ( + + ))} + + ); }; export default CompatSsoLoginList; diff --git a/frontend/src/components/GraphQLError.tsx b/frontend/src/components/GraphQLError.tsx new file mode 100644 index 00000000..b2889625 --- /dev/null +++ b/frontend/src/components/GraphQLError.tsx @@ -0,0 +1,24 @@ +// 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 { CombinedError } from "@urql/core"; +import { Alert } from "@vector-im/compound-web"; + +const GraphQLError: React.FC<{ error: CombinedError }> = ({ error }) => ( + + {error.toString()} + +); + +export default GraphQLError; diff --git a/frontend/src/components/NotFound.tsx b/frontend/src/components/NotFound.tsx new file mode 100644 index 00000000..5081fbe3 --- /dev/null +++ b/frontend/src/components/NotFound.tsx @@ -0,0 +1,19 @@ +// 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 } from "@vector-im/compound-web"; + +const NotFound: React.FC = () => ; + +export default NotFound; diff --git a/frontend/src/components/NotLoggedIn.tsx b/frontend/src/components/NotLoggedIn.tsx new file mode 100644 index 00000000..575a591d --- /dev/null +++ b/frontend/src/components/NotLoggedIn.tsx @@ -0,0 +1,21 @@ +// 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 } from "@vector-im/compound-web"; + +const NotLoggedIn: React.FC = () => ( + +); + +export default NotLoggedIn; diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index 9cc950d6..07422b15 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -41,7 +41,7 @@ const FRAGMENT = graphql(/* GraphQL */ ` `); const END_SESSION_MUTATION = graphql(/* GraphQL */ ` - mutation EndSession($id: ID!) { + mutation EndOAuth2Session($id: ID!) { endOauth2Session(input: { oauth2SessionId: $id }) { status oauth2Session { diff --git a/frontend/src/components/OAuth2SessionList.tsx b/frontend/src/components/OAuth2SessionList.tsx index 52bbe548..027b69ab 100644 --- a/frontend/src/components/OAuth2SessionList.tsx +++ b/frontend/src/components/OAuth2SessionList.tsx @@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; import { useTransition } from "react"; +import { mapQueryAtom } from "../atoms"; import { graphql } from "../gql"; import { PageInfo } from "../gql/graphql"; import { @@ -24,8 +25,10 @@ import { atomWithPagination, Pagination, } from "../pagination"; +import { isErr, isOk, unwrapErr, unwrapOk } from "../result"; import BlockList from "./BlockList"; +import GraphQLError from "./GraphQLError"; import OAuth2Session from "./OAuth2Session"; import PaginationControls from "./PaginationControls"; import { Title } from "./Typography"; @@ -68,18 +71,23 @@ const QUERY = graphql(/* GraphQL */ ` const currentPaginationAtom = atomForCurrentPagination(); const oauth2SessionListFamily = atomFamily((userId: string) => { - const oauth2SessionList = atomWithQuery({ + const oauth2SessionListQuery = atomWithQuery({ query: QUERY, getVariables: (get) => ({ userId, ...get(currentPaginationAtom) }), }); + const oauth2SessionList = mapQueryAtom( + oauth2SessionListQuery, + (data) => data.user?.oauth2Sessions || null + ); + return oauth2SessionList; }); const pageInfoFamily = atomFamily((userId: string) => { const pageInfoAtom = atom(async (get): Promise => { const result = await get(oauth2SessionListFamily(userId)); - return result.data?.user?.oauth2Sessions?.pageInfo ?? null; + return (isOk(result) && unwrapOk(result)?.pageInfo) || null; }); return pageInfoAtom; @@ -103,30 +111,30 @@ const OAuth2SessionList: React.FC = ({ userId }) => { const setPagination = useSetAtom(currentPaginationAtom); const [prevPage, nextPage] = useAtomValue(paginationFamily(userId)); + if (isErr(result)) return ; + const oauth2Sessions = unwrapOk(result); + if (oauth2Sessions === null) + return <>Failed to load OAuth 2.0 session list; + const paginate = (pagination: Pagination): void => { startTransition(() => { setPagination(pagination); }); }; - if (result.data?.user?.oauth2Sessions) { - const data = result.data.user.oauth2Sessions; - return ( - - List of OAuth 2.0 sessions: - paginate(prevPage) : null} - onNext={nextPage ? (): void => paginate(nextPage) : null} - disabled={pending} - /> - {data.edges.map((n) => ( - - ))} - - ); - } else { - return <>Failed to load OAuth 2.0 session list; - } + return ( + + List of OAuth 2.0 sessions: + paginate(prevPage) : null} + onNext={nextPage ? (): void => paginate(nextPage) : null} + disabled={pending} + /> + {oauth2Sessions.edges.map((n) => ( + + ))} + + ); }; export default OAuth2SessionList; diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index b8fcaf21..f81da6cc 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -21,6 +21,8 @@ const documents = { 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 mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": + types.EndBrowserSessionDocument, "\n query BrowserSessionList(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.BrowserSessionListDocument, "\n fragment CompatSsoLogin_login on CompatSsoLogin {\n id\n redirectUri\n createdAt\n session {\n id\n ...CompatSsoLogin_session\n createdAt\n deviceId\n finishedAt\n }\n }\n": @@ -33,8 +35,8 @@ const documents = { types.CompatSsoLoginListDocument, "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n client {\n id\n clientId\n clientName\n clientUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc, - "\n mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n": - types.EndSessionDocument, + "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n": + types.EndOAuth2SessionDocument, "\n query OAuth2SessionListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n oauth2Sessions(\n first: $first\n after: $after\n last: $last\n before: $before\n ) {\n edges {\n cursor\n node {\n id\n ...OAuth2Session_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.OAuth2SessionListQueryDocument, "\n fragment UserEmail_email on UserEmail {\n id\n email\n createdAt\n confirmedAt\n }\n": @@ -97,6 +99,12 @@ export function graphql( export function graphql( source: "\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n }\n" ): typeof documents["\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\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 mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n" +): typeof documents["\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -137,8 +145,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 mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n" -): typeof documents["\n mutation EndSession($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"]; + source: "\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n" +): typeof documents["\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index 6ee7372d..e06b637a 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -184,6 +184,28 @@ export type CreationEvent = { createdAt: Scalars["DateTime"]["output"]; }; +/** The input of the `endBrowserSession` mutation. */ +export type EndBrowserSessionInput = { + /** The ID of the session to end. */ + browserSessionId: Scalars["ID"]["input"]; +}; + +export type EndBrowserSessionPayload = { + __typename?: "EndBrowserSessionPayload"; + /** Returns the ended session. */ + browserSession?: Maybe; + /** The status of the mutation. */ + status: EndBrowserSessionStatus; +}; + +/** The status of the `endBrowserSession` mutation. */ +export enum EndBrowserSessionStatus { + /** The session was ended. */ + Ended = "ENDED", + /** The session was not found. */ + NotFound = "NOT_FOUND", +} + /** The input of the `endCompatSession` mutation. */ export type EndCompatSessionInput = { /** The ID of the session to end. */ @@ -243,6 +265,7 @@ export type Mutation = { __typename?: "Mutation"; /** Add an email address to the specified user */ addEmail: AddEmailPayload; + endBrowserSession: EndBrowserSessionPayload; endCompatSession: EndCompatSessionPayload; endOauth2Session: EndOAuth2SessionPayload; /** Remove an email address */ @@ -260,6 +283,11 @@ export type MutationAddEmailArgs = { input: AddEmailInput; }; +/** The mutations root of the GraphQL interface. */ +export type MutationEndBrowserSessionArgs = { + input: EndBrowserSessionInput; +}; + /** The mutations root of the GraphQL interface. */ export type MutationEndCompatSessionArgs = { input: EndCompatSessionInput; @@ -777,6 +805,25 @@ export type BrowserSession_SessionFragment = { } | null; } & { " $fragmentName"?: "BrowserSession_SessionFragment" }; +export type EndBrowserSessionMutationVariables = Exact<{ + id: Scalars["ID"]["input"]; +}>; + +export type EndBrowserSessionMutation = { + __typename?: "Mutation"; + endBrowserSession: { + __typename?: "EndBrowserSessionPayload"; + status: EndBrowserSessionStatus; + browserSession?: + | ({ __typename?: "BrowserSession"; id: string } & { + " $fragmentRefs"?: { + BrowserSession_SessionFragment: BrowserSession_SessionFragment; + }; + }) + | null; + }; +}; + export type BrowserSessionListQueryVariables = Exact<{ userId: Scalars["ID"]["input"]; first?: InputMaybe; @@ -908,11 +955,11 @@ export type OAuth2Session_SessionFragment = { }; } & { " $fragmentName"?: "OAuth2Session_SessionFragment" }; -export type EndSessionMutationVariables = Exact<{ +export type EndOAuth2SessionMutationVariables = Exact<{ id: Scalars["ID"]["input"]; }>; -export type EndSessionMutation = { +export type EndOAuth2SessionMutation = { __typename?: "Mutation"; endOauth2Session: { __typename?: "EndOAuth2SessionPayload"; @@ -1532,6 +1579,103 @@ export const AddEmailDocument = { }, ], } as unknown as DocumentNode; +export const EndBrowserSessionDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "mutation", + name: { kind: "Name", value: "EndBrowserSession" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { kind: "Variable", name: { kind: "Name", value: "id" } }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }, + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "endBrowserSession" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "input" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "browserSessionId" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "id" }, + }, + }, + ], + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "status" } }, + { + kind: "Field", + name: { kind: "Name", value: "browserSession" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "FragmentSpread", + name: { kind: "Name", value: "BrowserSession_session" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + kind: "FragmentDefinition", + name: { kind: "Name", value: "BrowserSession_session" }, + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "BrowserSession" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "createdAt" } }, + { + kind: "Field", + name: { kind: "Name", value: "lastAuthentication" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { kind: "Field", name: { kind: "Name", value: "createdAt" } }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + EndBrowserSessionMutation, + EndBrowserSessionMutationVariables +>; export const BrowserSessionListDocument = { kind: "Document", definitions: [ @@ -2054,13 +2198,13 @@ export const CompatSsoLoginListDocument = { CompatSsoLoginListQuery, CompatSsoLoginListQueryVariables >; -export const EndSessionDocument = { +export const EndOAuth2SessionDocument = { kind: "Document", definitions: [ { kind: "OperationDefinition", operation: "mutation", - name: { kind: "Name", value: "EndSession" }, + name: { kind: "Name", value: "EndOAuth2Session" }, variableDefinitions: [ { kind: "VariableDefinition", @@ -2151,7 +2295,10 @@ export const EndSessionDocument = { }, }, ], -} as unknown as DocumentNode; +} as unknown as DocumentNode< + EndOAuth2SessionMutation, + EndOAuth2SessionMutationVariables +>; export const OAuth2SessionListQueryDocument = { kind: "Document", definitions: [ diff --git a/frontend/src/gql/schema.ts b/frontend/src/gql/schema.ts index 7e2a42c5..2ddc4b05 100644 --- a/frontend/src/gql/schema.ts +++ b/frontend/src/gql/schema.ts @@ -522,6 +522,33 @@ export default { }, ], }, + { + kind: "OBJECT", + name: "EndBrowserSessionPayload", + fields: [ + { + name: "browserSession", + type: { + kind: "OBJECT", + name: "BrowserSession", + ofType: null, + }, + args: [], + }, + { + name: "status", + type: { + kind: "NON_NULL", + ofType: { + kind: "SCALAR", + name: "Any", + }, + }, + args: [], + }, + ], + interfaces: [], + }, { kind: "OBJECT", name: "EndCompatSessionPayload", @@ -637,6 +664,29 @@ export default { }, ], }, + { + name: "endBrowserSession", + type: { + kind: "NON_NULL", + ofType: { + kind: "OBJECT", + name: "EndBrowserSessionPayload", + ofType: null, + }, + }, + args: [ + { + name: "input", + type: { + kind: "NON_NULL", + ofType: { + kind: "SCALAR", + name: "Any", + }, + }, + }, + ], + }, { name: "endCompatSession", type: { diff --git a/frontend/src/pages/Account.tsx b/frontend/src/pages/Account.tsx index de42e4b2..72a06a39 100644 --- a/frontend/src/pages/Account.tsx +++ b/frontend/src/pages/Account.tsx @@ -15,8 +15,11 @@ import { useAtomValue } from "jotai"; import { currentUserIdAtom } from "../atoms"; +import GraphQLError from "../components/GraphQLError"; +import NotLoggedIn from "../components/NotLoggedIn"; import UserEmailList from "../components/UserEmailList"; import UserGreeting from "../components/UserGreeting"; +import { isErr, unwrapErr, unwrapOk } from "../result"; const UserAccount: React.FC<{ id: string }> = ({ id }) => { return ( @@ -28,16 +31,17 @@ const UserAccount: React.FC<{ id: string }> = ({ id }) => { }; const CurrentUserAccount: React.FC = () => { - const userId = useAtomValue(currentUserIdAtom); - if (userId !== null) { - return ( -
- -
- ); - } + const result = useAtomValue(currentUserIdAtom); + if (isErr(result)) return ; - return
Not logged in.
; + const userId = unwrapOk(result); + if (userId === null) return ; + + return ( +
+ +
+ ); }; export default CurrentUserAccount; diff --git a/frontend/src/pages/BrowserSession.tsx b/frontend/src/pages/BrowserSession.tsx index 07704daf..07ac03d3 100644 --- a/frontend/src/pages/BrowserSession.tsx +++ b/frontend/src/pages/BrowserSession.tsx @@ -16,7 +16,11 @@ import { useAtomValue } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; +import { mapQueryAtom } from "../atoms"; +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 BrowserSessionQuery($id: ID!) { @@ -36,26 +40,31 @@ const QUERY = graphql(/* GraphQL */ ` `); const browserSessionFamily = atomFamily((id: string) => { - const browserSessionAtom = atomWithQuery({ + 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 ; - if (result.data?.browserSession) { - return ( -
-        {JSON.stringify(result.data.browserSession, null, 2)}
-      
- ); - } + const browserSession = unwrapOk(result); + if (browserSession === null) return ; - return <>Failed to load browser session; + return ( +
+      {JSON.stringify(browserSession, null, 2)}
+    
+ ); }; export default BrowserSession; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index c2df1999..2ad7e3b2 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -12,32 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Alert } from "@vector-im/compound-web"; import { useAtomValue } from "jotai"; import { currentUserIdAtom } from "../atoms"; import BrowserSessionList from "../components/BrowserSessionList"; import CompatSsoLoginList from "../components/CompatSsoLoginList"; +import GraphQLError from "../components/GraphQLError"; +import NotLoggedIn from "../components/NotLoggedIn"; import OAuth2SessionList from "../components/OAuth2SessionList"; import UserGreeting from "../components/UserGreeting"; +import { isErr, unwrapErr, unwrapOk } from "../result"; const Home: React.FC = () => { - const currentUserId = useAtomValue(currentUserIdAtom); + const result = useAtomValue(currentUserIdAtom); + if (isErr(result)) return ; - if (currentUserId) { - return ( - <> - -
- - - -
- - ); - } else { - return ; - } + const currentUserId = unwrapOk(result); + if (currentUserId === null) return ; + + return ( + <> + +
+ + + +
+ + ); }; export default Home; diff --git a/frontend/src/pages/OAuth2Client.tsx b/frontend/src/pages/OAuth2Client.tsx index d331ddb7..8ea92a3c 100644 --- a/frontend/src/pages/OAuth2Client.tsx +++ b/frontend/src/pages/OAuth2Client.tsx @@ -16,7 +16,11 @@ import { useAtomValue } from "jotai"; import { atomFamily } from "jotai/utils"; import { atomWithQuery } from "jotai-urql"; +import { mapQueryAtom } from "../atoms"; +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!) { @@ -33,26 +37,31 @@ const QUERY = graphql(/* GraphQL */ ` `); const oauth2ClientFamily = atomFamily((id: string) => { - const oauth2ClientAtom = atomWithQuery({ + 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 ; - if (result.data?.oauth2Client) { - return ( -
-        {JSON.stringify(result.data.oauth2Client, null, 2)}
-      
- ); - } + const oauth2Client = unwrapOk(result); + if (oauth2Client === null) return ; - return <>Failed to load OAuth2 client; + return ( +
+      {JSON.stringify(oauth2Client, null, 2)}
+    
+ ); }; export default OAuth2Client; diff --git a/frontend/src/result.ts b/frontend/src/result.ts new file mode 100644 index 00000000..7b9dcd21 --- /dev/null +++ b/frontend/src/result.ts @@ -0,0 +1,61 @@ +// 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. + +const RESULT = Symbol("Result"); +const ERR = Symbol("Err"); +const OK = Symbol("Ok"); + +/** + * An `Ok` is a type that represents a successful result. + */ +export type Ok = { + [RESULT]: typeof OK; + [OK]: T; +}; + +/** + * An `Err` is a type that represents an error. + */ +export type Err = { + [RESULT]: typeof ERR; + [ERR]: E; +}; + +/** + * A `Result` is a type that represents either an `Ok` or an `Err`. + */ +export type Result = Ok | Err; + +// Construct an `Ok` +export const ok = (data: T): Ok => ({ [RESULT]: OK, [OK]: data }); + +// Construct an `Err` +export const err = (error: E): Err => ({ + [RESULT]: ERR, + [ERR]: error, +}); + +// Check if a `Result` is an `Ok` +export const isOk = (result: Result): result is Ok => + result[RESULT] === OK; + +// Check if a `Result` is an `Err` +export const isErr = (result: Result): result is Err => + result[RESULT] === ERR; + +// Extract the data from an `Ok` +export const unwrapOk = (result: Ok): T => result[OK]; + +// Extract the error from an `Err` +export const unwrapErr = (result: Err): E => result[ERR]; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 57d1e3fa..52d8f727 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -16,8 +16,9 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import codegen from "vite-plugin-graphql-codegen"; +import svgr from "vite-plugin-svgr"; -export default defineConfig({ +export default defineConfig((env) => ({ base: "/app/", build: { manifest: true, @@ -29,11 +30,50 @@ export default defineConfig({ react({ babel: { plugins: [ - "jotai/babel/plugin-react-refresh", - "jotai/babel/plugin-debug-label", + [ + "jotai/babel/plugin-react-refresh", + { + customAtomNames: [ + "mapQueryAtom", + "atomWithPagination", + "atomWithCurrentPagination", + ], + }, + ], + [ + "jotai/babel/plugin-debug-label", + { + customAtomNames: [ + "mapQueryAtom", + "atomWithPagination", + "atomWithCurrentPagination", + ], + }, + ], ], }, }), + + svgr({ + exportAsDefault: true, + + esbuildOptions: { + // This makes sure we're using the same JSX runtime as React itself + jsx: "automatic", + jsxDev: env.mode === "development", + }, + + svgrOptions: { + // Using 1em in order to make SVG size inherits from text size. + icon: "1em", + + svgProps: { + // Adding a class in case we want to add global overrides, but one + // should probably stick to using CSS modules most of the time + className: "cpd-icon", + }, + }, + }), ], server: { proxy: { @@ -50,4 +90,4 @@ export default defineConfig({ all: true, }, }, -}); +}));