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 (
+
+
+`;
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
+
+
+`;
+
+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;
+};