diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 14203caa7..aad058c02 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -50,8 +50,7 @@ } }, "browser_session_details": { - "current_badge": "Current", - "session_details_title": "Session" + "current_badge": "Current" }, "browser_sessions_overview": { "body:one": "{{count}} active session", @@ -60,7 +59,7 @@ "view_all_button": "View all" }, "compat_session_detail": { - "client_details_title": "Client", + "client_details_title": "Client info", "name": "Name", "session_details_title": "Session" }, @@ -92,16 +91,14 @@ "not_found_alert_title": "Not found.", "not_logged_in_alert": "You're not logged in.", "oauth2_client_detail": { - "details_title": "Client", - "id": "Client ID", + "details_title": "Client info", "name": "Name", "policy": "Policy", "terms": "Terms of service" }, "oauth2_session_detail": { "client_details_name": "Name", - "client_title": "Client", - "session_details_title": "Session" + "client_title": "Client info" }, "pagination_controls": { "total": "Total: {{totalCount}}" @@ -120,21 +117,19 @@ } }, "session": { + "client_id_label": "Client ID", "current": "Current", "device_id_label": "Device ID", "finished_label": "Finished", - "id_label": "ID", "ip_label": "IP Address", "last_active_label": "Last Active", - "last_auth_label": "Last Authentication", "name_for_platform": "{{name}} for {{platform}}", "scopes_label": "Scopes", "signed_in_label": "Signed in", + "title": "Device details", "unknown_browser": "Unknown browser", "unknown_device": "Unknown device", - "uri_label": "Uri", - "user_id_label": "User ID", - "username_label": "User name" + "uri_label": "Uri" }, "session_detail": { "alert": { @@ -186,5 +181,16 @@ }, "resend_code": "Resend code" } + }, + "mas": { + "scope": { + "edit_profile": "Edit your profile and contact details", + "manage_sessions": "Manage your devices and sessions", + "mas_admin": "Administer any user on the matrix-authentication-service", + "send_messages": "Send new messages on your behalf", + "synapse_admin": "Administer the Synapse homeserver", + "view_messages": "View your existing messages and data", + "view_profile": "See your profile info and contact details" + } } } diff --git a/frontend/src/components/Block/Block.module.css b/frontend/src/components/Block/Block.module.css index 40f1e9e51..4ce50d83e 100644 --- a/frontend/src/components/Block/Block.module.css +++ b/frontend/src/components/Block/Block.module.css @@ -17,9 +17,17 @@ width: 100%; color: var(--cpd-color-text-primary); padding-bottom: var(--cpd-space-5x); - border-bottom: 1px solid var(--cpd-color-border-interactive-secondary); &:last-child { border-bottom: none; } -} \ No newline at end of file +} + +.title { + padding-bottom: var(--cpd-space-2x); + border-bottom: var(--cpd-border-width-2) solid var(--cpd-color-gray-400); + + /* Workaround compound design tokens heading style being broken */ + font-weight: var(--cpd-font-weight-semibold) !important; + font-size: var(--cpd-font-size-heading-sm) !important; +} diff --git a/frontend/src/components/Block/Block.test.tsx b/frontend/src/components/Block/Block.test.tsx index 8e9f6795b..bbc9abd1c 100644 --- a/frontend/src/components/Block/Block.test.tsx +++ b/frontend/src/components/Block/Block.test.tsx @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// @vitest-environment happy-dom + import { create } from "react-test-renderer"; import { describe, expect, it } from "vitest"; diff --git a/frontend/src/components/Block/Block.tsx b/frontend/src/components/Block/Block.tsx index bc660248d..14782467f 100644 --- a/frontend/src/components/Block/Block.tsx +++ b/frontend/src/components/Block/Block.tsx @@ -12,18 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Heading } from "@vector-im/compound-web"; import cx from "classnames"; +import { ReactNode } from "react"; import styles from "./Block.module.css"; type Props = React.PropsWithChildren<{ + title?: ReactNode; className?: string; highlight?: boolean; }>; -const Block: React.FC = ({ children, className, highlight }) => { +const Block: React.FC = ({ children, className, highlight, title }) => { return (
+ {title && ( + + {title} + + )} + {children}
); diff --git a/frontend/src/components/BlockList/BlockList.module.css b/frontend/src/components/BlockList/BlockList.module.css index 8e0f533d0..e024ad39d 100644 --- a/frontend/src/components/BlockList/BlockList.module.css +++ b/frontend/src/components/BlockList/BlockList.module.css @@ -17,5 +17,5 @@ display: flex; flex-direction: column; align-content: flex-start; - gap: var(--cpd-space-5x); -} \ No newline at end of file + gap: var(--cpd-space-8x); +} diff --git a/frontend/src/components/BlockList/BlockList.test.tsx b/frontend/src/components/BlockList/BlockList.test.tsx index cc9326891..9cc82eec1 100644 --- a/frontend/src/components/BlockList/BlockList.test.tsx +++ b/frontend/src/components/BlockList/BlockList.test.tsx @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +// @vitest-environment happy-dom + import { create } from "react-test-renderer"; import { describe, expect, it } from "vitest"; diff --git a/frontend/src/components/Client/OAuth2ClientDetail.tsx b/frontend/src/components/Client/OAuth2ClientDetail.tsx index f25a0e946..f49f3435c 100644 --- a/frontend/src/components/Client/OAuth2ClientDetail.tsx +++ b/frontend/src/components/Client/OAuth2ClientDetail.tsx @@ -57,10 +57,6 @@ const OAuth2ClientDetail: React.FC = ({ client }) => { const details = [ { label: t("frontend.oauth2_client_detail.name"), value: data.clientName }, - { - label: t("frontend.oauth2_client_detail.id"), - value: {data.clientId}, - }, { label: t("frontend.oauth2_client_detail.terms"), value: data.tosUri && , diff --git a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap index ae825aed6..11ca3ecb1 100644 --- a/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap +++ b/frontend/src/components/Client/__snapshots__/OAuth2ClientDetail.test.tsx.snap @@ -17,89 +17,65 @@ exports[` > renders client details 1`] = `
-
- Client -
- + client.org/policy + +
+ diff --git a/frontend/src/components/Session/LastActive.tsx b/frontend/src/components/Session/LastActive.tsx index a841ca8cf..b4a7d3010 100644 --- a/frontend/src/components/Session/LastActive.tsx +++ b/frontend/src/components/Session/LastActive.tsx @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import cx from "classnames"; import { differenceInSeconds, parseISO } from "date-fns"; import { useTranslation } from "react-i18next"; @@ -27,7 +28,8 @@ const INACTIVE_MIN_AGE = 60 * 60 * 24 * 90; const LastActive: React.FC<{ lastActive: Date | string; now?: Date | string; -}> = ({ lastActive: lastActiveProps, now: nowProps }) => { + className?: string; +}> = ({ lastActive: lastActiveProps, now: nowProps, className }) => { const { t } = useTranslation(); const lastActive = @@ -44,21 +46,21 @@ const LastActive: React.FC<{ const formattedDate = formatDate(lastActive); if (differenceInSeconds(now, lastActive) <= ACTIVE_NOW_MAX_AGE) { return ( - + {t("frontend.last_active.active_now")} ); } if (differenceInSeconds(now, lastActive) > INACTIVE_MIN_AGE) { return ( - + {t("frontend.last_active.inactive_90_days")} ); } const relativeDate = formatReadableDate(lastActive, now); return ( - + {t("frontend.last_active.active_date", { relativeDate })} ); diff --git a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx index 4be29d96e..aec6dcb03 100644 --- a/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/BrowserSessionDetail.tsx @@ -21,7 +21,6 @@ import BlockList from "../BlockList/BlockList"; import { useEndBrowserSession } from "../BrowserSession"; import DateTime from "../DateTime"; import EndSessionButton from "../Session/EndSessionButton"; -import LastActive from "../Session/LastActive"; import styles from "./BrowserSessionDetail.module.css"; import SessionDetails from "./SessionDetails"; @@ -81,54 +80,7 @@ const BrowserSessionDetail: React.FC = ({ session, isCurrent }) => { ] : []; - const lastActiveIp = data.lastActiveIp - ? [ - { - label: t("frontend.session.ip_label"), - value: {data.lastActiveIp}, - }, - ] - : []; - - const lastActiveAt = data.lastActiveAt - ? [ - { - label: t("frontend.session.last_active_label"), - value: , - }, - ] - : []; - - const lastAuthentication = data.lastAuthentication - ? [ - { - label: t("frontend.session.last_auth_label"), - value: ( - - ), - }, - ] - : []; - - const sessionDetails = [ - { label: t("frontend.session.id_label"), value: {data.id} }, - { - label: t("frontend.session.user_id_label"), - value: {data.user.id}, - }, - { - label: t("frontend.session.username_label"), - value: {data.user.username}, - }, - { - label: t("frontend.session.signed_in_label"), - value: , - }, - ...finishedAt, - ...lastActiveAt, - ...lastActiveIp, - ...lastAuthentication, - ]; + const sessionDetails = [...finishedAt]; return ( @@ -139,7 +91,14 @@ const BrowserSessionDetail: React.FC = ({ session, isCurrent }) => { )} {sessionName} {!data.finishedAt && } diff --git a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx index 7b5d3a4f6..93718585e 100644 --- a/frontend/src/components/SessionDetail/CompatSessionDetail.tsx +++ b/frontend/src/components/SessionDetail/CompatSessionDetail.tsx @@ -22,7 +22,6 @@ import { END_SESSION_MUTATION, simplifyUrl } from "../CompatSession"; import DateTime from "../DateTime"; import ExternalLink from "../ExternalLink/ExternalLink"; import EndSessionButton from "../Session/EndSessionButton"; -import LastActive from "../Session/LastActive"; import SessionDetails from "./SessionDetails"; import SessionHeader from "./SessionHeader"; @@ -69,38 +68,7 @@ const CompatSessionDetail: React.FC = ({ session }) => { ] : []; - const lastActiveIp = data.lastActiveIp - ? [ - { - label: t("frontend.session.ip_label"), - value: {data.lastActiveIp}, - }, - ] - : []; - - const lastActiveAt = data.lastActiveAt - ? [ - { - label: t("frontend.session.last_active_label"), - value: , - }, - ] - : []; - - const sessionDetails = [ - { label: t("frontend.session.id_label"), value: {data.id} }, - { - label: t("frontend.session.device_id_label"), - value: {data.deviceId}, - }, - { - label: t("frontend.session.signed_in_label"), - value: , - }, - ...finishedAt, - ...lastActiveAt, - ...lastActiveIp, - ]; + const sessionDetails = [...finishedAt]; const clientDetails: { label: string; value: string | JSX.Element }[] = []; @@ -124,6 +92,10 @@ const CompatSessionDetail: React.FC = ({ session }) => { {data.deviceId || data.id} {clientDetails.length > 0 ? ( diff --git a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx index e440f2727..d48782535 100644 --- a/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx +++ b/frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx @@ -24,7 +24,6 @@ import { Link } from "../Link"; import { END_SESSION_MUTATION } from "../OAuth2Session"; import ClientAvatar from "../Session/ClientAvatar"; import EndSessionButton from "../Session/EndSessionButton"; -import LastActive from "../Session/LastActive"; import SessionDetails from "./SessionDetails"; import SessionHeader from "./SessionHeader"; @@ -62,8 +61,6 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { const deviceId = getDeviceIdFromScope(data.scope); - const scopes = data.scope.split(" "); - const finishedAt = data.finishedAt ? [ { @@ -73,48 +70,7 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { ] : []; - const lastActiveIp = data.lastActiveIp - ? [ - { - label: t("frontend.session.ip_label"), - value: {data.lastActiveIp}, - }, - ] - : []; - - const lastActiveAt = data.lastActiveAt - ? [ - { - label: t("frontend.session.last_active_label"), - value: , - }, - ] - : []; - - const sessionDetails = [ - { label: t("frontend.session.id_label"), value: {data.id} }, - { - label: t("frontend.session.device_id_label"), - value: {deviceId}, - }, - { - label: t("frontend.session.signed_in_label"), - value: , - }, - ...finishedAt, - ...lastActiveAt, - ...lastActiveIp, - { - label: t("frontend.session.scopes_label"), - value: ( - - {scopes.map((scope) => ( - {scope} - ))} - - ), - }, - ]; + const sessionDetails = [...finishedAt]; const clientTitle = ( @@ -136,7 +92,7 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { ), }, { - label: t("frontend.session.id_label"), + label: t("frontend.session.client_id_label"), value: {data.client.clientId}, }, { @@ -157,7 +113,12 @@ const OAuth2SessionDetail: React.FC = ({ session }) => { {deviceId || data.id} diff --git a/frontend/src/components/SessionDetail/SessionDetails.module.css b/frontend/src/components/SessionDetail/SessionDetails.module.css index 0da69e9c7..ab72b91c3 100644 --- a/frontend/src/components/SessionDetail/SessionDetails.module.css +++ b/frontend/src/components/SessionDetail/SessionDetails.module.css @@ -13,28 +13,51 @@ * limitations under the License. */ -.list { - display: flex; - flex-direction: column; - margin-top: var(--cpd-space-1x); - gap: var(--cpd-space-1x); +.wrapper { + display: flex; + flex-wrap: wrap; + gap: var(--cpd-space-4x); + margin-bottom: var(--cpd-space-4x); + margin-top: var(--cpd-space-8x); } -.detail-row { - display: flex; - flex-direction: row; - gap: var(--cpd-space-4x); +.wrapper h5 { + color: var(--cpd-color-text-secondary); } -.detail-label { - flex: 0 0 20%; - color: var(--cpd-color-text-secondary); +.wrapper .datum { + width: max-content; } -.detail-value { - overflow-wrap: anywhere; - display: flex; - flex-direction: row; - align-items: center; - gap: var(--cpd-space-1x); +.datum { + flex-grow: 1; + max-width: 100%; +} + +.datum-value { + font-size: var(--cpd-font-size-body-md); + text-overflow: ellipsis; + overflow: hidden; +} + +.scope-list { + display: flex; + flex-direction: column; + gap: var(--cpd-space-scale); + border-radius: var(--cpd-space-5x); + overflow: hidden; +} + +.scope { + background: var(--cpd-color-bg-subtle-secondary); + padding: var(--cpd-space-3x) var(--cpd-space-5x); + display: flex; + align-items: center; + gap: var(--cpd-space-3x); +} + +.scope svg { + inline-size: var(--cpd-space-6x); + block-size: var(--cpd-space-6x); + color: var(--cpd-color-icon-tertiary); } diff --git a/frontend/src/components/SessionDetail/SessionDetails.tsx b/frontend/src/components/SessionDetail/SessionDetails.tsx index 0299d3918..25a8da02e 100644 --- a/frontend/src/components/SessionDetail/SessionDetails.tsx +++ b/frontend/src/components/SessionDetail/SessionDetails.tsx @@ -1,4 +1,4 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. +// Copyright 2022-2024 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. @@ -12,39 +12,155 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { H6, Body } from "@vector-im/compound-web"; +import IconChat from "@vector-im/compound-design-tokens/icons/chat.svg?react"; +import IconComputer from "@vector-im/compound-design-tokens/icons/computer.svg?react"; +import IconError from "@vector-im/compound-design-tokens/icons/error.svg?react"; +import IconInfo from "@vector-im/compound-design-tokens/icons/info.svg?react"; +import IconSend from "@vector-im/compound-design-tokens/icons/send.svg?react"; +import IconUserProfile from "@vector-im/compound-design-tokens/icons/user-profile.svg?react"; +import { Text } from "@vector-im/compound-web"; import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; import Block from "../Block/Block"; +import DateTime from "../DateTime"; +import LastActive from "../Session/LastActive"; import styles from "./SessionDetails.module.css"; -type Detail = { label: string; value: string | ReactNode }; +export type Detail = { label: string; value: ReactNode }; type Props = { title: string | ReactNode; - details: Detail[]; + lastActive?: Date; + signedIn?: Date; + deviceId?: string; + ipAddress?: string; + scopes?: string[]; + details?: Detail[]; }; -const DetailRow: React.FC = ({ label, value }) => ( -
  • - - {label} - - - {value} - -
  • -); - -const SessionDetails: React.FC = ({ title, details }) => { +const Scope: React.FC<{ scope: string }> = ({ scope }) => { + const { t } = useTranslation(); + // Filter out "urn:matrix:org.matrix.msc2967.client:device:" + if (scope.startsWith("urn:matrix:org.matrix.msc2967.client:device:")) { + return null; + } + + // Needs to be manually kept in sync with /templates/components/scope.html + const scopeMap: Record = { + openid: [[IconUserProfile, t("mas.scope.view_profile")]], + "urn:mas:graphql:*": [ + [IconInfo, t("mas.scope.edit_profile")], + [IconComputer, t("mas.scope.manage_sessions")], + ], + "urn:matrix:org.matrix.msc2967.client:api:*": [ + [IconChat, t("mas.scope.view_messages")], + [IconSend, t("mas.scope.send_messages")], + ], + "urn:synapse:admin:*": [[IconError, t("mas.scope.synapse_admin")]], + "urn:mas:admin": [[IconError, t("mas.scope.mas_admin")]], + }; + + const mappedScopes: [typeof IconInfo, string][] = scopeMap[scope] ?? [ + [IconInfo, scope], + ]; + + return ( + <> + {mappedScopes.map(([Icon, text], i) => ( +
  • + + + {text} + +
  • + ))} + + ); +}; + +const Datum: React.FC<{ label: string; value: ReactNode }> = ({ + label, + value, +}) => { return ( - -
    {title}
    -
      - {details.map(({ label, value }) => ( - +
      + + {label} + + {typeof value === "string" ? ( + + {value} + + ) : ( + value + )} +
      + ); +}; + +const SessionDetails: React.FC = ({ + title, + lastActive, + signedIn, + deviceId, + ipAddress, + details, + scopes, +}) => { + const { t } = useTranslation(); + + return ( + +
      + {lastActive && ( + + } + /> + )} + {signedIn && ( + + } + /> + )} + {deviceId && ( + + )} + {ipAddress && ( + {ipAddress}} + /> + )} + {details?.map(({ label, value }) => ( + ))} -
    + + + {scopes?.length && ( + + {scopes.map((scope) => ( + + ))} + + } + /> + )}
    ); }; diff --git a/frontend/src/components/SessionDetail/SessionHeader.module.css b/frontend/src/components/SessionDetail/SessionHeader.module.css index 806aa192e..1c19e86a8 100644 --- a/frontend/src/components/SessionDetail/SessionHeader.module.css +++ b/frontend/src/components/SessionDetail/SessionHeader.module.css @@ -17,14 +17,14 @@ display: flex; flex-direction: row; justify-content: flex-start; - gap: var(--cpd-space-2x); + gap: var(--cpd-space-4x); align-items: center; } .back-button { display: block; - inline-size: var(--cpd-space-8x); - block-size: var(--cpd-space-8x); + inline-size: var(--cpd-space-7x); + block-size: var(--cpd-space-7x); /* the icon is 0.75 the size of the button, so add padding to put it in the middle */ padding: var(--cpd-space-1x); @@ -35,13 +35,13 @@ cursor: pointer; border-radius: 50%; position: relative; - background: transparent; + background-color: var(--cpd-color-bg-subtle-secondary); line-height: 0px; } .back-button svg { - inline-size: var(--cpd-space-6x); - block-size: var(--cpd-space-6x); + inline-size: var(--cpd-space-5x); + block-size: var(--cpd-space-5x); } .back-button[aria-disabled="true"] { diff --git a/frontend/src/components/SessionDetail/SessionHeader.tsx b/frontend/src/components/SessionDetail/SessionHeader.tsx index 224b933e7..91e41fc30 100644 --- a/frontend/src/components/SessionDetail/SessionHeader.tsx +++ b/frontend/src/components/SessionDetail/SessionHeader.tsx @@ -13,7 +13,7 @@ // limitations under the License. import { Link } from "@tanstack/react-router"; -import IconArrowLeft from "@vector-im/compound-design-tokens/icons/arrow-left.svg?react"; +import IconChevronLeft from "@vector-im/compound-design-tokens/icons/chevron-left.svg?react"; import { H3 } from "@vector-im/compound-web"; import styles from "./SessionHeader.module.css"; @@ -25,7 +25,7 @@ const SessionHeader: React.FC> = ({ return (
    - +

    {children}

    diff --git a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap index d6ad13fee..38d8d7340 100644 --- a/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap +++ b/frontend/src/components/SessionDetail/__snapshots__/CompatSessionDetail.test.tsx.snap @@ -21,7 +21,7 @@ exports[` > renders a compatability session details 1`] = ` xmlns="http://www.w3.org/2000/svg" > @@ -34,148 +34,118 @@ exports[` > renders a compatability session details 1`] = `
    -
    Session -
    -
      +
      -
    • -

      - ID -

      -

      - - session-id - -

      -
    • -
    • + + Inactive for 90+ days + +
    • +
      -

      - Device ID -

      -

      - - abcd1234 - -

      - -
    • -

      Signed in -

      -

      - -

      -
    • -
    • + +
    • +
      -

      - Last Active -

      + Device ID +

      - - Inactive for 90+ days - + abcd1234

      - -
    • +
      -

      IP Address -

      -

      - - 1.2.3.4 - -

      -
    • -
    + + + 1.2.3.4 + +
    +
    -
    - Client -
    - + + + https://element.io + +
    +