diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index ca5232a77..7d0f73959 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -53,6 +53,9 @@ module.exports = { alphabetize: { order: "asc" }, }, ], + // override enzyme deprecation from matrix-eslint + // as it causes errors + "deprecate/import": "off", }, settings: { "import/resolver": { diff --git a/frontend/src/components/Block/Block.module.css b/frontend/src/components/Block/Block.module.css index d3daa431b..59e4d6089 100644 --- a/frontend/src/components/Block/Block.module.css +++ b/frontend/src/components/Block/Block.module.css @@ -14,8 +14,11 @@ */ .block { - background-color: var(--cpd-color-bg-subtle-secondary); color: var(--cpd-color-text-primary); - padding: var(--cpd-space-4x); - border-radius: var(--cpd-space-2x); + padding: var(--cpd-space-5x) 0; + border-bottom: 1px solid var(--cpd-color-border-interactive-secondary); + + &:last-child { + border-bottom: none; + } } \ No newline at end of file diff --git a/frontend/src/components/BlockList/BlockList.module.css b/frontend/src/components/BlockList/BlockList.module.css index dd28f6e95..e78e1bc9c 100644 --- a/frontend/src/components/BlockList/BlockList.module.css +++ b/frontend/src/components/BlockList/BlockList.module.css @@ -16,6 +16,5 @@ .block-list { display: flex; flex-direction: column; - gap: var(--cpd-space-4x); align-content: flex-start; } \ No newline at end of file diff --git a/frontend/src/components/CompatSession.test.tsx b/frontend/src/components/CompatSession.test.tsx new file mode 100644 index 000000000..2df86ae69 --- /dev/null +++ b/frontend/src/components/CompatSession.test.tsx @@ -0,0 +1,57 @@ +// 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 { create } from "react-test-renderer"; +import { describe, expect, it, vi } from "vitest"; + +import { FragmentType } from "../gql/fragment-masking"; + +import CompatSession, { COMPAT_SESSION_FRAGMENT } from "./CompatSession"; +import DateTime from "./DateTime"; + +// Mock out datetime to avoid timezones/date formatting +vi.mock("./DateTime", () => { + const MockDateTime: typeof DateTime = ({ datetime }) => ( + {datetime.toString()} + ); + return { default: MockDateTime }; +}); + +describe("", () => { + const session = { + id: "session-id", + deviceId: "abcd1234", + createdAt: "2023-06-29T03:35:17.451292+00:00", + ssoLogin: { + id: "test-id", + redirectUri: "https://element.io", + }, + } as FragmentType; + + const finishedAt = "2023-06-29T03:35:19.451292+00:00"; + + it("renders an active session", () => { + const component = create(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("renders a finished session", () => { + const finishedSession = { + ...session, + finishedAt, + }; + const component = create(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/CompatSession.tsx b/frontend/src/components/CompatSession.tsx index b92975c2d..6a80f18c9 100644 --- a/frontend/src/components/CompatSession.tsx +++ b/frontend/src/components/CompatSession.tsx @@ -20,10 +20,9 @@ import { useTransition } from "react"; import { FragmentType, graphql, useFragment } from "../gql"; -import Block from "./Block"; -import DateTime from "./DateTime"; -import { Body, Bold, Code } from "./Typography"; +import { Session } from "./Session"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const LOGIN_FRAGMENT = graphql(/* GraphQL */ ` fragment CompatSession_sso_login on CompatSsoLogin { id @@ -31,7 +30,7 @@ const LOGIN_FRAGMENT = graphql(/* GraphQL */ ` } `); -const FRAGMENT = graphql(/* GraphQL */ ` +export const COMPAT_SESSION_FRAGMENT = graphql(/* GraphQL */ ` fragment CompatSession_session on CompatSession { id createdAt @@ -44,6 +43,17 @@ const FRAGMENT = graphql(/* GraphQL */ ` } `); +type CompatSessionType = { + id: string; + deviceId: string; + createdAt: string; + finishedAt: string | null; + ssoLogin: { + id: string; + redirectUri: string; + }; +}; + const END_SESSION_MUTATION = graphql(/* GraphQL */ ` mutation EndCompatSession($id: ID!) { endCompatSession(input: { compatSessionId: $id }) { @@ -69,10 +79,13 @@ const endCompatSessionFamily = atomFamily((id: string) => { }); const CompatSession: React.FC<{ - session: FragmentType; + session: FragmentType; }> = ({ session }) => { const [pending, startTransition] = useTransition(); - const data = useFragment(FRAGMENT, session); + const data = useFragment( + COMPAT_SESSION_FRAGMENT, + session, + ) as CompatSessionType; const endCompatSession = useSetAtom(endCompatSessionFamily(data.id)); const onSessionEnd = (): void => { @@ -82,45 +95,24 @@ const CompatSession: React.FC<{ }; return ( - - - Started: - - {data.finishedAt ? ( -
- Finished: -
- ) : null} - - Device ID: {data.deviceId} - - {data.ssoLogin && } - {data.finishedAt ? null : ( + + {!data.finishedAt && ( )} -
- ); -}; - -const CompatSsoLogin: React.FC<{ - login: FragmentType; -}> = ({ login }) => { - const data = useFragment(LOGIN_FRAGMENT, login); - - return ( - <> - - Redirect URI: {data.redirectUri} - - + ); }; diff --git a/frontend/src/components/OAuth2Session.test.tsx b/frontend/src/components/OAuth2Session.test.tsx new file mode 100644 index 000000000..39c10023e --- /dev/null +++ b/frontend/src/components/OAuth2Session.test.tsx @@ -0,0 +1,62 @@ +// 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 { create } from "react-test-renderer"; +import { describe, expect, it, vi } from "vitest"; + +import { FragmentType } from "../gql/fragment-masking"; + +import DateTime from "./DateTime"; +import OAuth2Session, { OAUTH2_SESSION_FRAGMENT } from "./OAuth2Session"; + +// Mock out datetime to avoid timezones/date formatting +vi.mock("./DateTime", () => { + const MockDateTime: typeof DateTime = ({ datetime }) => ( + {datetime.toString()} + ); + return { default: MockDateTime }; +}); + +describe("", () => { + const defaultProps = { + session: { + id: "session-id", + scope: + "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234", + createdAt: "2023-06-29T03:35:17.451292+00:00", + client: { + id: "test-id", + clientId: "test-client-id", + clientName: "Element", + clientUri: "https://element.io", + }, + } as FragmentType, + }; + + const finishedAt = "2023-06-29T03:35:19.451292+00:00"; + + it("renders an active session", () => { + const component = create(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("renders a finished session", () => { + const finishedSession = { + ...defaultProps.session, + finishedAt, + }; + const component = create(); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/OAuth2Session.tsx b/frontend/src/components/OAuth2Session.tsx index 1e5290412..2f1a0a5b4 100644 --- a/frontend/src/components/OAuth2Session.tsx +++ b/frontend/src/components/OAuth2Session.tsx @@ -18,14 +18,13 @@ import { atomFamily } from "jotai/utils"; import { atomWithMutation } from "jotai-urql"; import { useTransition } from "react"; -import { Link } from "../Router"; import { FragmentType, graphql, useFragment } from "../gql"; +import { getDeviceIdFromScope } from "../utils/deviceIdFromScope"; -import Block from "./Block"; -import DateTime from "./DateTime"; -import Typography, { Body, Bold, Code } from "./Typography"; +// import LoadingSpinner from "./LoadingSpinner/LoadingSpinner"; +import { Session } from "./Session"; -const FRAGMENT = graphql(/* GraphQL */ ` +export const OAUTH2_SESSION_FRAGMENT = graphql(/* GraphQL */ ` fragment OAuth2Session_session on Oauth2Session { id scope @@ -40,6 +39,19 @@ const FRAGMENT = graphql(/* GraphQL */ ` } `); +type Oauth2SessionType = { + id: string; + scope: string; + createdAt: string; + finishedAt: string | null; + client: { + id: string; + clientId: string; + clientName: string; + clientUri: string; + }; +}; + const END_SESSION_MUTATION = graphql(/* GraphQL */ ` mutation EndOAuth2Session($id: ID!) { endOauth2Session(input: { oauth2SessionId: $id }) { @@ -65,87 +77,48 @@ const endSessionFamily = atomFamily((id: string) => { }); type Props = { - session: FragmentType; -}; - -const API_SCOPE = "urn:matrix:org.matrix.msc2967.client:api:*"; -const DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"; - -const Scope: React.FC<{ scope: string }> = ({ scope }) => { - if (scope === "openid") return <>OpenID; - if (scope === "email") return <>Email; - if (scope === "profile") return <>Profile; - if (scope === API_SCOPE) return <>Matrix C-S API ; - - if (scope.startsWith(DEVICE_PREFIX)) - return ( - <> - Device {scope.slice(DEVICE_PREFIX.length)} - - ); - - return {scope}; + session: FragmentType; }; const OAuth2Session: React.FC = ({ session }) => { const [pending, startTransition] = useTransition(); - const data = useFragment(FRAGMENT, session); + const data = useFragment( + OAUTH2_SESSION_FRAGMENT, + session, + ) as Oauth2SessionType; const endSession = useSetAtom(endSessionFamily(data.id)); + // @TODO(kerrya) make this wait for session refresh properly + // https://github.com/matrix-org/matrix-authentication-service/issues/1533 const onSessionEnd = (): void => { startTransition(() => { endSession(); }); }; + const sessionName = getDeviceIdFromScope(data.scope); + return ( - - - - Client ID: {data.client.clientId} - - - {data.client.clientName && ( - - Client name: {data.client.clientName} - - )} - - Started - - {data.finishedAt && ( -

- Finished -

- )} -
-
- - Access: - -
    - {data.scope.split(" ").map((scope) => ( -
  • - -
  • - ))} -
-
+ {!data.finishedAt && ( )} -
+ ); }; diff --git a/frontend/src/components/Session/Session.module.css b/frontend/src/components/Session/Session.module.css new file mode 100644 index 000000000..d9327e384 --- /dev/null +++ b/frontend/src/components/Session/Session.module.css @@ -0,0 +1,36 @@ +/* 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. + */ + +.session-name { + overflow: hidden; + text-overflow: ellipsis; + margin-top: var(--cpd-space-2x); + margin-bottom: var(--cpd-space-1x); +} + +.session-metadata { + color: var(--cpd-color-text-secondary); + + &[data-finished] { + color: var(--cpd-color-text-critical-primary); + } +} + +.session-actions { + margin-top: var(--cpd-space-1x); + display: flex; + flex-direction: row; + justify-content: flex-end; +} \ No newline at end of file diff --git a/frontend/src/components/Session/Session.test.tsx b/frontend/src/components/Session/Session.test.tsx new file mode 100644 index 000000000..927188214 --- /dev/null +++ b/frontend/src/components/Session/Session.test.tsx @@ -0,0 +1,69 @@ +// 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 { create } from "react-test-renderer"; +import { describe, expect, it, vi } from "vitest"; + +import DateTime from "../DateTime"; + +import Session from "./Session"; + +// Mock out datetime to avoid timezones/date formatting +vi.mock("../DateTime", () => { + const MockDateTime: typeof DateTime = ({ datetime }) => ( + {datetime.toString()} + ); + return { default: MockDateTime }; +}); + +describe("", () => { + const defaultProps = { + id: "session-id", + createdAt: "2023-06-29T03:35:17.451292+00:00", + }; + + const finishedAt = "2023-06-29T03:35:19.451292+00:00"; + + it("renders an active session", () => { + const component = create(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("renders a finished session", () => { + const component = create( + , + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("uses session name when truthy", () => { + const name = "test session name"; + const component = create( + , + ); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("uses client name when truthy", () => { + const clientName = "Element"; + const component = create( + , + ); + expect(component.toJSON()).toMatchSnapshot(); + }); +}); diff --git a/frontend/src/components/Session/Session.tsx b/frontend/src/components/Session/Session.tsx new file mode 100644 index 000000000..79e378a46 --- /dev/null +++ b/frontend/src/components/Session/Session.tsx @@ -0,0 +1,68 @@ +// 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 { H6, Body } from "@vector-im/compound-web"; + +import Block from "../Block"; +import DateTime from "../DateTime"; + +import styles from "./Session.module.css"; + +const SessionMetadata: React.FC> = ( + props, +) => ; + +export type SessionProps = { + id: string; + name?: string; + createdAt: string; + finishedAt?: string; + clientName?: string; +}; +const Session: React.FC> = ({ + id, + name, + createdAt, + finishedAt, + clientName, + children, +}) => { + return ( + +
+ {name || id} +
+ + + Signed in + + {!!finishedAt && ( + + Finished + + )} + {!!clientName && ( + + Client:{" "} + + {clientName} + + + )} + {!!children &&
{children}
} +
+ ); +}; + +export default Session; diff --git a/frontend/src/components/Session/__snapshots__/Session.test.tsx.snap b/frontend/src/components/Session/__snapshots__/Session.test.tsx.snap new file mode 100644 index 000000000..9a5baf2c1 --- /dev/null +++ b/frontend/src/components/Session/__snapshots__/Session.test.tsx.snap @@ -0,0 +1,123 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders a finished session 1`] = ` +
+

+ session-id +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Finished + + 2023-06-29T03:35:19.451292+00:00 + +

+
+`; + +exports[` > renders an active session 1`] = ` +
+

+ session-id +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+
+`; + +exports[` > uses client name when truthy 1`] = ` +
+

+ session-id +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Finished + + 2023-06-29T03:35:19.451292+00:00 + +

+

+ Client: + + + Element + +

+
+`; + +exports[` > uses session name when truthy 1`] = ` +
+

+ test session name +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Finished + + 2023-06-29T03:35:19.451292+00:00 + +

+
+`; diff --git a/frontend/src/components/Session/index.ts b/frontend/src/components/Session/index.ts new file mode 100644 index 000000000..e81d73e4b --- /dev/null +++ b/frontend/src/components/Session/index.ts @@ -0,0 +1,15 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { default as Session } from "./Session"; diff --git a/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap new file mode 100644 index 000000000..73a770ae2 --- /dev/null +++ b/frontend/src/components/__snapshots__/CompatSession.test.tsx.snap @@ -0,0 +1,89 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders a finished session 1`] = ` +
+

+ abcd1234 +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Finished + + 2023-06-29T03:35:19.451292+00:00 + +

+

+ Client: + + + https://element.io + +

+
+`; + +exports[` > renders an active session 1`] = ` +
+

+ abcd1234 +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Client: + + + https://element.io + +

+
+ +
+
+`; diff --git a/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap new file mode 100644 index 000000000..2aa2ac025 --- /dev/null +++ b/frontend/src/components/__snapshots__/OAuth2Session.test.tsx.snap @@ -0,0 +1,89 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders a finished session 1`] = ` +
+

+ abcd1234 +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Finished + + 2023-06-29T03:35:19.451292+00:00 + +

+

+ Client: + + + Element + +

+
+`; + +exports[` > renders an active session 1`] = ` +
+

+ abcd1234 +

+

+ Signed in + + 2023-06-29T03:35:17.451292+00:00 + +

+

+ Client: + + + Element + +

+
+ +
+
+`; diff --git a/frontend/src/utils/deviceIdFromScope.test.ts b/frontend/src/utils/deviceIdFromScope.test.ts new file mode 100644 index 000000000..5fe81d65b --- /dev/null +++ b/frontend/src/utils/deviceIdFromScope.test.ts @@ -0,0 +1,31 @@ +/* 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 { describe, it, expect } from "vitest"; + +import { getDeviceIdFromScope } from "./deviceIdFromScope"; + +describe("getDeviceIdFromScope()", () => { + it("returns deviceid when device is part of scope", () => { + const scope = + "openid urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:abcd1234"; + expect(getDeviceIdFromScope(scope)).toEqual("abcd1234"); + }); + + it("returns undefined when device not part of scope", () => { + const scope = "openid some:other:scope "; + expect(getDeviceIdFromScope(scope)).toBeUndefined(); + }); +}); diff --git a/frontend/src/utils/deviceIdFromScope.ts b/frontend/src/utils/deviceIdFromScope.ts new file mode 100644 index 000000000..cb6c04bc8 --- /dev/null +++ b/frontend/src/utils/deviceIdFromScope.ts @@ -0,0 +1,27 @@ +/* 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 DEVICE_PREFIX = "urn:matrix:org.matrix.msc2967.client:device:"; + +/** + * Device scopes are suffixed with the deviceId + * Isolate the suffix so we can display it + * @param scope the full scope of the session + * @returns deviceId, or undefined when not a device scope + */ +export const getDeviceIdFromScope = (scope: string): string | undefined => { + const [, deviceId] = scope.split(DEVICE_PREFIX); + return deviceId; +};