Add header showing total number of devices

This commit is contained in:
Eric Eastwood
2026-04-17 17:49:20 -05:00
parent 3042a64ae9
commit d1edf64dda
7 changed files with 39 additions and 12 deletions
+1 -1
View File
@@ -2898,7 +2898,7 @@
"type": "object",
"properties": {
"soft_limit": {
"description": "Upon login in interactive contexts (like OAuth 2.0 sessions), if the\n soft limit is reached, it will display a policy violation screen\n (web UI) to remove sessions before creating the new session.\n\n This is not enforced in non-interactive contexts (like the legacy\n compability login API) as there is no opportunity for us to show\n some UI for people remove some sessions. See [`hard_limit`] for\n enforcement on that side.\n\n [`hard_limit`]: Self::hard_limit",
"description": "Upon login in interactive contexts (like OAuth 2.0 sessions), if the\n soft limit is reached, it will display a policy violation screen\n (web UI) to remove sessions before creating the new session.\n\n This is not enforced in non-interactive contexts (like the legacy\n compability login API) as there is no opportunity for us to show\n some UI for people remove some sessions. See [`hard_limit`] for\n enforcement on that side.\n\n This is the limit that is displayed in the UI\n\n [`hard_limit`]: Self::hard_limit",
"type": "integer",
"format": "uint64",
"minimum": 1
+3 -1
View File
@@ -293,7 +293,9 @@
"no_active_sessions": {
"default": "You are not signed in to any application.",
"inactive_90_days": "All your sessions have been active in the last 90 days."
}
},
"num_devices_filtered_header": "{{filtered_count}} devices match your filter ({{total_count}} total)",
"num_devices_header": "{{num_devices}} devices"
},
"verify_email": {
"code_expired_alert": {
+3 -3
View File
@@ -52,7 +52,7 @@ type Documents = {
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": typeof types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\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 }\n }\n": typeof types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": typeof types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": typeof types.OAuth2ClientDocument,
@@ -109,7 +109,7 @@ const documents: Documents = {
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": types.UserProfileDocument,
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": types.PlanManagementTabDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\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 }\n }\n": types.BrowserSessionListDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n": types.SessionsOverviewDocument,
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": types.CurrentUserGreetingDocument,
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
@@ -280,7 +280,7 @@ export function graphql(source: "\n query BrowserSessionList(\n $first: Int\
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n"): typeof import('./graphql').SessionsOverviewDocument;
export function graphql(source: "\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n\n # Get the total count of active app sessions before any filtering\n unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {\n totalCount\n }\n }\n }\n }\n"): typeof import('./graphql').SessionsOverviewDocument;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
+4 -1
View File
@@ -1896,7 +1896,7 @@ export type SessionsOverviewQueryVariables = Exact<{ [key: string]: never; }>;
export type SessionsOverviewQuery = { __typename?: 'Query', viewer:
| { __typename: 'Anonymous' }
| (
{ __typename: 'User', id: string }
{ __typename: 'User', id: string, unfilteredAppSessions: { __typename?: 'AppSessionConnection', totalCount: number } }
& { ' $fragmentRefs'?: { 'BrowserSessionsOverview_UserFragment': BrowserSessionsOverview_UserFragment } }
)
};
@@ -2705,6 +2705,9 @@ export const SessionsOverviewDocument = new TypedDocumentString(`
... on User {
id
...BrowserSessionsOverview_user
unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {
totalCount
}
}
}
}
-1
View File
@@ -4,7 +4,6 @@
* Please see LICENSE files in the repository root for full details.
*/
.nav-bar-error-icon {
display: inline-block;
@@ -6,7 +6,7 @@
import { queryOptions, useSuspenseQuery } from "@tanstack/react-query";
import { notFound } from "@tanstack/react-router";
import { H3 } from "@vector-im/compound-web";
import { H3, H4 } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";
import * as v from "valibot";
import { ButtonLink } from "../components/ButtonLink";
@@ -36,6 +36,11 @@ const QUERY = graphql(/* GraphQL */ `
... on User {
id
...BrowserSessionsOverview_user
# Get the total count of active app sessions before any filtering
unfilteredAppSessions: appSessions(first: 1, state: ACTIVE) {
totalCount
}
}
}
}
@@ -136,9 +141,9 @@ function Sessions(): React.ReactElement {
const { t } = useTranslation();
const { inactive, pagination } = Route.useLoaderDeps();
const {
data: { viewer },
data: { viewer: overviewViewer },
} = useSuspenseQuery(query);
if (viewer.__typename !== "User") throw notFound();
if (overviewViewer.__typename !== "User") throw notFound();
const { data } = useSuspenseQuery(listQuery(pagination, inactive));
if (data.viewer.__typename !== "User") throw notFound();
@@ -153,10 +158,25 @@ function Sessions(): React.ReactElement {
// We reverse the list as we are paginating backwards
const edges = [...appSessions.edges].reverse();
// By default, we just show a "X devices" header
let deviceHeaderText = t("frontend.user_sessions_overview.num_devices_header", {
num_devices: appSessions.totalCount,
});
// But if we're showing a filtered down view, we want to explain how many devices you
// filtered down to and how many total unfilterd devices there are total.
if (overviewViewer.unfilteredAppSessions.totalCount != appSessions.totalCount) {
deviceHeaderText = t("frontend.user_sessions_overview.num_devices_filtered_header", {
filtered_count: appSessions.totalCount,
total_count: overviewViewer.unfilteredAppSessions.totalCount,
});
}
return (
<div className="flex flex-col gap-6">
<H3>{t("frontend.user_sessions_overview.heading")}</H3>
<BrowserSessionsOverview user={viewer} />
<BrowserSessionsOverview user={overviewViewer} />
<H4>{deviceHeaderText}</H4>
<Separator kind="section" />
<div className="flex gap-2 justify-start items-center">
<Filter
+4 -1
View File
@@ -68,7 +68,10 @@ function Account(): React.ReactElement {
<NavBar>
<NavItem to="/">{t("frontend.nav.settings")}</NavItem>
<NavItem to="/sessions">{t("frontend.nav.devices")} <IconErrorSolid className={styles.navBarErrorIcon} /></NavItem>
<NavItem to="/sessions">
{t("frontend.nav.devices")}{" "}
<IconErrorSolid className={styles.navBarErrorIcon} />
</NavItem>
{planManagementIframeUri && (
<NavItem to="/plan">{t("frontend.nav.plan")}</NavItem>
)}