You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-11-23 11:02:35 +03:00
Make the session list way better
This commit is contained in:
@@ -119,21 +119,19 @@
|
|||||||
"title": "Crypto identity reset temporarily allowed"
|
"title": "Crypto identity reset temporarily allowed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selectable_session": {
|
|
||||||
"label": "Select session"
|
|
||||||
},
|
|
||||||
"session": {
|
"session": {
|
||||||
"current_badge": "Current",
|
"current": "Current",
|
||||||
"device_id_label": "Device ID",
|
"device_id_label": "Device ID",
|
||||||
"finished_date": "Finished <datetime/>",
|
|
||||||
"finished_label": "Finished",
|
"finished_label": "Finished",
|
||||||
"id_label": "ID",
|
"id_label": "ID",
|
||||||
"ip_label": "IP Address",
|
"ip_label": "IP Address",
|
||||||
"last_active_label": "Last Active",
|
"last_active_label": "Last Active",
|
||||||
"last_auth_label": "Last Authentication",
|
"last_auth_label": "Last Authentication",
|
||||||
|
"name_for_platform": "{{name}} for {{platform}}",
|
||||||
"scopes_label": "Scopes",
|
"scopes_label": "Scopes",
|
||||||
"signed_in_date": "Signed in <datetime/>",
|
|
||||||
"signed_in_label": "Signed in",
|
"signed_in_label": "Signed in",
|
||||||
|
"unknown_browser": "Unknown browser",
|
||||||
|
"unknown_device": "Unknown device",
|
||||||
"uri_label": "Uri",
|
"uri_label": "Uri",
|
||||||
"user_id_label": "User ID",
|
"user_id_label": "User ID",
|
||||||
"username_label": "User name"
|
"username_label": "User name"
|
||||||
|
|||||||
@@ -12,14 +12,19 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
|
import { Badge } from "@vector-im/compound-web";
|
||||||
import { parseISO } from "date-fns";
|
import { parseISO } from "date-fns";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "urql";
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
|
import { DeviceType } from "../gql/graphql";
|
||||||
|
|
||||||
|
import DateTime from "./DateTime";
|
||||||
import EndSessionButton from "./Session/EndSessionButton";
|
import EndSessionButton from "./Session/EndSessionButton";
|
||||||
import Session from "./Session/Session";
|
import LastActive from "./Session/LastActive";
|
||||||
|
import * as Card from "./SessionCard";
|
||||||
|
|
||||||
const FRAGMENT = graphql(/* GraphQL */ `
|
const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment BrowserSession_session on BrowserSession {
|
fragment BrowserSession_session on BrowserSession {
|
||||||
@@ -77,38 +82,84 @@ type Props = {
|
|||||||
|
|
||||||
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
const BrowserSession: React.FC<Props> = ({ session, isCurrent }) => {
|
||||||
const data = useFragment(FRAGMENT, session);
|
const data = useFragment(FRAGMENT, session);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
const onSessionEnd = useEndBrowserSession(data.id, isCurrent);
|
||||||
|
|
||||||
|
const deviceType = data.userAgent?.deviceType ?? DeviceType.Unknown;
|
||||||
|
|
||||||
|
let deviceName: string | null = null;
|
||||||
|
let clientName: string | null = null;
|
||||||
|
|
||||||
|
// If we have a model, use that as the device name, and the browser (+ OS) as the client name
|
||||||
|
if (data.userAgent?.model) {
|
||||||
|
deviceName = data.userAgent.model;
|
||||||
|
if (data.userAgent?.name) {
|
||||||
|
if (data.userAgent?.os) {
|
||||||
|
clientName = t("frontend.session.name_for_platform", {
|
||||||
|
name: data.userAgent.name,
|
||||||
|
platform: data.userAgent.os,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clientName = data.userAgent.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Else use the browser as the device name
|
||||||
|
deviceName = data.userAgent?.name ?? t("frontend.session.unknown_browser");
|
||||||
|
// and if we have an OS, use that as the client name
|
||||||
|
clientName = data.userAgent?.os ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
const createdAt = parseISO(data.createdAt);
|
const createdAt = parseISO(data.createdAt);
|
||||||
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
|
|
||||||
const lastActiveAt = data.lastActiveAt
|
const lastActiveAt = data.lastActiveAt
|
||||||
? parseISO(data.lastActiveAt)
|
? parseISO(data.lastActiveAt)
|
||||||
: undefined;
|
: undefined;
|
||||||
let sessionName = "Browser session";
|
|
||||||
if (data.userAgent) {
|
|
||||||
if (data.userAgent.model && data.userAgent.name) {
|
|
||||||
sessionName = `${data.userAgent.name} on ${data.userAgent.model}`;
|
|
||||||
} else if (data.userAgent.name && data.userAgent.os) {
|
|
||||||
sessionName = `${data.userAgent.name} on ${data.userAgent.os}`;
|
|
||||||
} else if (data.userAgent.name) {
|
|
||||||
sessionName = data.userAgent.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Card.Root>
|
||||||
id={data.id}
|
<Card.LinkBody
|
||||||
name={sessionName}
|
to="/sessions/$id"
|
||||||
createdAt={createdAt}
|
params={{ id: data.id }}
|
||||||
finishedAt={finishedAt}
|
disabled={!!data.finishedAt}
|
||||||
isCurrent={isCurrent}
|
|
||||||
deviceType={data.userAgent?.deviceType}
|
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
|
||||||
lastActiveAt={lastActiveAt}
|
|
||||||
>
|
>
|
||||||
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
|
<Card.Header type={deviceType}>
|
||||||
</Session>
|
<Card.Name name={deviceName} />
|
||||||
|
{clientName && <Card.Client name={clientName} />}
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Metadata>
|
||||||
|
{lastActiveAt && !isCurrent && (
|
||||||
|
<Card.Info label={t("frontend.session.last_active_label")}>
|
||||||
|
<LastActive lastActive={lastActiveAt} />
|
||||||
|
</Card.Info>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card.Info label={t("frontend.session.signed_in_label")}>
|
||||||
|
<DateTime datetime={createdAt} />
|
||||||
|
</Card.Info>
|
||||||
|
|
||||||
|
{isCurrent && (
|
||||||
|
<Badge kind="success" className="self-center">
|
||||||
|
{t("frontend.session.current")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Card.Metadata>
|
||||||
|
</Card.LinkBody>
|
||||||
|
|
||||||
|
{!data.finishedAt && (
|
||||||
|
<Card.Action>
|
||||||
|
<EndSessionButton endSession={onSessionEnd}>
|
||||||
|
<Card.Body compact>
|
||||||
|
<Card.Header type={deviceType}>
|
||||||
|
<Card.Name name={deviceName} />
|
||||||
|
{clientName && <Card.Client name={clientName} />}
|
||||||
|
</Card.Header>
|
||||||
|
</Card.Body>
|
||||||
|
</EndSessionButton>
|
||||||
|
</Card.Action>
|
||||||
|
)}
|
||||||
|
</Card.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { parseISO } from "date-fns";
|
import { parseISO } from "date-fns";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "urql";
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
|
import { DeviceType } from "../gql/graphql";
|
||||||
|
|
||||||
import { Session } from "./Session";
|
import DateTime from "./DateTime";
|
||||||
import EndSessionButton from "./Session/EndSessionButton";
|
import EndSessionButton from "./Session/EndSessionButton";
|
||||||
|
import LastActive from "./Session/LastActive";
|
||||||
|
import * as Card from "./SessionCard";
|
||||||
|
|
||||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment CompatSession_session on CompatSession {
|
fragment CompatSession_session on CompatSession {
|
||||||
@@ -29,7 +33,6 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
lastActiveIp
|
lastActiveIp
|
||||||
lastActiveAt
|
lastActiveAt
|
||||||
userAgent {
|
userAgent {
|
||||||
raw
|
|
||||||
name
|
name
|
||||||
os
|
os
|
||||||
model
|
model
|
||||||
@@ -78,6 +81,7 @@ export const simplifyUrl = (url: string): string => {
|
|||||||
const CompatSession: React.FC<{
|
const CompatSession: React.FC<{
|
||||||
session: FragmentType<typeof FRAGMENT>;
|
session: FragmentType<typeof FRAGMENT>;
|
||||||
}> = ({ session }) => {
|
}> = ({ session }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const data = useFragment(FRAGMENT, session);
|
const data = useFragment(FRAGMENT, session);
|
||||||
const [, endCompatSession] = useMutation(END_SESSION_MUTATION);
|
const [, endCompatSession] = useMutation(END_SESSION_MUTATION);
|
||||||
|
|
||||||
@@ -85,39 +89,68 @@ const CompatSession: React.FC<{
|
|||||||
await endCompatSession({ id: data.id });
|
await endCompatSession({ id: data.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
let clientName = data.ssoLogin?.redirectUri
|
const clientName = data.ssoLogin?.redirectUri
|
||||||
? simplifyUrl(data.ssoLogin.redirectUri)
|
? simplifyUrl(data.ssoLogin.redirectUri)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (data.userAgent) {
|
const deviceType = data.userAgent?.deviceType ?? DeviceType.Unknown;
|
||||||
if (data.userAgent.model && data.userAgent.name) {
|
|
||||||
clientName = `${data.userAgent.name} on ${data.userAgent.model}`;
|
const deviceName =
|
||||||
} else if (data.userAgent.name && data.userAgent.os) {
|
data.userAgent?.model ??
|
||||||
clientName = `${data.userAgent.name} on ${data.userAgent.os}`;
|
(data.userAgent?.name
|
||||||
} else if (data.userAgent.name) {
|
? data.userAgent?.os
|
||||||
clientName = data.userAgent.name;
|
? t("frontend.session.name_for_platform", {
|
||||||
}
|
name: data.userAgent.name,
|
||||||
}
|
platform: data.userAgent.os,
|
||||||
|
})
|
||||||
|
: data.userAgent.name
|
||||||
|
: t("frontend.session.unknown_device"));
|
||||||
|
|
||||||
const createdAt = parseISO(data.createdAt);
|
const createdAt = parseISO(data.createdAt);
|
||||||
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
|
|
||||||
const lastActiveAt = data.lastActiveAt
|
const lastActiveAt = data.lastActiveAt
|
||||||
? parseISO(data.lastActiveAt)
|
? parseISO(data.lastActiveAt)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Card.Root>
|
||||||
id={data.id}
|
<Card.LinkBody
|
||||||
name={data.deviceId}
|
to="/sessions/$id"
|
||||||
createdAt={createdAt}
|
params={{ id: data.id }}
|
||||||
finishedAt={finishedAt}
|
disabled={!!data.finishedAt}
|
||||||
clientName={clientName}
|
|
||||||
deviceType={data.userAgent?.deviceType}
|
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
|
||||||
lastActiveAt={lastActiveAt}
|
|
||||||
>
|
>
|
||||||
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
|
<Card.Header type={deviceType}>
|
||||||
</Session>
|
<Card.Name name={deviceName} />
|
||||||
|
{clientName && <Card.Client name={clientName} />}
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Metadata>
|
||||||
|
{lastActiveAt && (
|
||||||
|
<Card.Info label={t("frontend.session.last_active_label")}>
|
||||||
|
<LastActive lastActive={lastActiveAt} />
|
||||||
|
</Card.Info>
|
||||||
|
)}
|
||||||
|
<Card.Info label={t("frontend.session.signed_in_label")}>
|
||||||
|
<DateTime datetime={createdAt} />
|
||||||
|
</Card.Info>
|
||||||
|
<Card.Info label={t("frontend.session.device_id_label")}>
|
||||||
|
{data.deviceId}
|
||||||
|
</Card.Info>
|
||||||
|
</Card.Metadata>
|
||||||
|
</Card.LinkBody>
|
||||||
|
|
||||||
|
{!data.finishedAt && (
|
||||||
|
<Card.Action>
|
||||||
|
<EndSessionButton endSession={onSessionEnd}>
|
||||||
|
<Card.Body compact>
|
||||||
|
<Card.Header type={deviceType}>
|
||||||
|
<Card.Name name={deviceName} />
|
||||||
|
{clientName && <Card.Client name={clientName} />}
|
||||||
|
</Card.Header>
|
||||||
|
</Card.Body>
|
||||||
|
</EndSessionButton>
|
||||||
|
</Card.Action>
|
||||||
|
)}
|
||||||
|
</Card.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import { parseISO } from "date-fns";
|
import { parseISO } from "date-fns";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMutation } from "urql";
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
import { FragmentType, graphql, useFragment } from "../gql";
|
import { FragmentType, graphql, useFragment } from "../gql";
|
||||||
import { DeviceType, Oauth2ApplicationType } from "../gql/graphql";
|
import { DeviceType, Oauth2ApplicationType } from "../gql/graphql";
|
||||||
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
|
import { getDeviceIdFromScope } from "../utils/deviceIdFromScope";
|
||||||
|
|
||||||
import { Session } from "./Session";
|
import DateTime from "./DateTime";
|
||||||
import EndSessionButton from "./Session/EndSessionButton";
|
import EndSessionButton from "./Session/EndSessionButton";
|
||||||
|
import LastActive from "./Session/LastActive";
|
||||||
|
import * as Card from "./SessionCard";
|
||||||
|
|
||||||
export const FRAGMENT = graphql(/* GraphQL */ `
|
export const FRAGMENT = graphql(/* GraphQL */ `
|
||||||
fragment OAuth2Session_session on Oauth2Session {
|
fragment OAuth2Session_session on Oauth2Session {
|
||||||
@@ -32,9 +35,9 @@ export const FRAGMENT = graphql(/* GraphQL */ `
|
|||||||
lastActiveAt
|
lastActiveAt
|
||||||
|
|
||||||
userAgent {
|
userAgent {
|
||||||
|
name
|
||||||
model
|
model
|
||||||
os
|
os
|
||||||
osVersion
|
|
||||||
deviceType
|
deviceType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +80,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const OAuth2Session: React.FC<Props> = ({ session }) => {
|
const OAuth2Session: React.FC<Props> = ({ session }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const data = useFragment(FRAGMENT, session);
|
const data = useFragment(FRAGMENT, session);
|
||||||
const [, endSession] = useMutation(END_SESSION_MUTATION);
|
const [, endSession] = useMutation(END_SESSION_MUTATION);
|
||||||
|
|
||||||
@@ -87,7 +91,6 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
|||||||
const deviceId = getDeviceIdFromScope(data.scope);
|
const deviceId = getDeviceIdFromScope(data.scope);
|
||||||
|
|
||||||
const createdAt = parseISO(data.createdAt);
|
const createdAt = parseISO(data.createdAt);
|
||||||
const finishedAt = data.finishedAt ? parseISO(data.finishedAt) : undefined;
|
|
||||||
const lastActiveAt = data.lastActiveAt
|
const lastActiveAt = data.lastActiveAt
|
||||||
? parseISO(data.lastActiveAt)
|
? parseISO(data.lastActiveAt)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -98,26 +101,67 @@ const OAuth2Session: React.FC<Props> = ({ session }) => {
|
|||||||
: data.userAgent?.deviceType) ??
|
: data.userAgent?.deviceType) ??
|
||||||
getDeviceTypeFromClientAppType(data.client.applicationType);
|
getDeviceTypeFromClientAppType(data.client.applicationType);
|
||||||
|
|
||||||
let clientName = data.client.clientName || data.client.clientId || undefined;
|
const clientName = data.client.clientName || data.client.clientId;
|
||||||
|
|
||||||
if (data.userAgent?.model) {
|
const deviceName =
|
||||||
clientName = `${clientName} on ${data.userAgent.model}`;
|
data.userAgent?.model ??
|
||||||
}
|
(data.userAgent?.name
|
||||||
|
? data.userAgent?.os
|
||||||
|
? t("frontend.session.name_for_platform", {
|
||||||
|
name: data.userAgent.name,
|
||||||
|
platform: data.userAgent.os,
|
||||||
|
})
|
||||||
|
: data.userAgent.name
|
||||||
|
: t("frontend.session.unknown_device"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Session
|
<Card.Root>
|
||||||
id={data.id}
|
<Card.LinkBody
|
||||||
name={deviceId}
|
to="/sessions/$id"
|
||||||
createdAt={createdAt}
|
params={{ id: data.id }}
|
||||||
finishedAt={finishedAt}
|
disabled={!!data.finishedAt}
|
||||||
clientName={clientName}
|
|
||||||
clientLogoUri={data.client.logoUri || undefined}
|
|
||||||
deviceType={deviceType}
|
|
||||||
lastActiveIp={data.lastActiveIp || undefined}
|
|
||||||
lastActiveAt={lastActiveAt}
|
|
||||||
>
|
>
|
||||||
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
|
<Card.Header type={deviceType}>
|
||||||
</Session>
|
<Card.Name name={deviceName} />
|
||||||
|
<Card.Client
|
||||||
|
name={clientName}
|
||||||
|
logoUri={data.client.logoUri ?? undefined}
|
||||||
|
/>
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Metadata>
|
||||||
|
{lastActiveAt && (
|
||||||
|
<Card.Info label={t("frontend.session.last_active_label")}>
|
||||||
|
<LastActive lastActive={lastActiveAt} />
|
||||||
|
</Card.Info>
|
||||||
|
)}
|
||||||
|
<Card.Info label={t("frontend.session.signed_in_label")}>
|
||||||
|
<DateTime datetime={createdAt} />
|
||||||
|
</Card.Info>
|
||||||
|
{deviceId && (
|
||||||
|
<Card.Info label={t("frontend.session.device_id_label")}>
|
||||||
|
{deviceId}
|
||||||
|
</Card.Info>
|
||||||
|
)}
|
||||||
|
</Card.Metadata>
|
||||||
|
</Card.LinkBody>
|
||||||
|
|
||||||
|
{!data.finishedAt && (
|
||||||
|
<Card.Action>
|
||||||
|
<EndSessionButton endSession={onSessionEnd}>
|
||||||
|
<Card.Body compact>
|
||||||
|
<Card.Header type={deviceType}>
|
||||||
|
<Card.Name name={deviceName} />
|
||||||
|
<Card.Client
|
||||||
|
name={clientName}
|
||||||
|
logoUri={data.client.logoUri ?? undefined}
|
||||||
|
/>
|
||||||
|
</Card.Header>
|
||||||
|
</Card.Body>
|
||||||
|
</EndSessionButton>
|
||||||
|
</Card.Action>
|
||||||
|
)}
|
||||||
|
</Card.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
color: var(--cpd-color-icon-primary);
|
color: var(--cpd-color-icon-secondary);
|
||||||
background-color: var(--cpd-color-bg-subtle-primary);
|
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||||
flex: 0 0 var(--cpd-space-4x);
|
|
||||||
box-sizing: content-box;
|
box-sizing: content-box;
|
||||||
height: var(--cpd-space-4x);
|
height: var(--cpd-space-6x);
|
||||||
width: var(--cpd-space-4x);
|
width: var(--cpd-space-6x);
|
||||||
padding: var(--cpd-space-2x);
|
padding: var(--cpd-space-2x);
|
||||||
border-radius: var(--cpd-space-2x);
|
border-radius: var(--cpd-space-2x);
|
||||||
}
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/* 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 {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap:var(--cpd-space-4x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 1 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-name {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
margin-top: var(--cpd-space-1x);
|
|
||||||
margin-bottom: var(--cpd-space-1x);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-metadata {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--cpd-space-1x);
|
|
||||||
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;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
// 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 type { Meta, StoryObj } from "@storybook/react";
|
|
||||||
import { Button } from "@vector-im/compound-web";
|
|
||||||
import { parseISO } from "date-fns";
|
|
||||||
import { ReactElement } from "react";
|
|
||||||
|
|
||||||
import BlockList from "../BlockList/BlockList";
|
|
||||||
|
|
||||||
import Session from "./Session";
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: "UI/Session/Session",
|
|
||||||
component: Session,
|
|
||||||
tags: ["autodocs"],
|
|
||||||
|
|
||||||
argTypes: {
|
|
||||||
createdAt: { control: { type: "date" } },
|
|
||||||
finishedAt: { control: { type: "date" } },
|
|
||||||
lastActiveAt: { control: { type: "date" } },
|
|
||||||
},
|
|
||||||
|
|
||||||
decorators: [
|
|
||||||
(Story): ReactElement => (
|
|
||||||
<div style={{ width: "378px" }}>
|
|
||||||
<BlockList>
|
|
||||||
<Story />
|
|
||||||
</BlockList>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
} satisfies Meta<typeof Session>;
|
|
||||||
|
|
||||||
export default meta;
|
|
||||||
type Story = StoryObj<typeof Session>;
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
id: "oauth2_session:01H5VAGA5NYTKJVXP3HMMKDJQ0",
|
|
||||||
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BasicSession: Story = {
|
|
||||||
args: {
|
|
||||||
...defaultProps,
|
|
||||||
name: "KlTqK9CRt3",
|
|
||||||
lastActiveIp: "2001:8003:c4614:f501:3091:888a:49c7",
|
|
||||||
lastActiveAt: parseISO("2023-07-29T03:35:17.451292+00:00"),
|
|
||||||
clientName: "Element",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BasicFinishedSession: Story = {
|
|
||||||
args: {
|
|
||||||
...defaultProps,
|
|
||||||
name: "Chrome on Android",
|
|
||||||
finishedAt: parseISO("2023-06-30T03:35:17.451292+00:00"),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithClientLogo: Story = {
|
|
||||||
args: {
|
|
||||||
...defaultProps,
|
|
||||||
name: "KlTqK9CRt3",
|
|
||||||
clientName: "Element",
|
|
||||||
clientLogoUri: "https://element.io/images/logo-mark-primary.svg",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithMinimumProps: Story = {
|
|
||||||
args: defaultProps,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WithChildActions: Story = {
|
|
||||||
args: {
|
|
||||||
...defaultProps,
|
|
||||||
name: "KlTqK9CRt3",
|
|
||||||
clientName: "Element",
|
|
||||||
clientLogoUri: "https://element.io/images/logo-mark-primary.svg",
|
|
||||||
children: (
|
|
||||||
<>
|
|
||||||
<Button size="sm" onClick={(): void => {}} kind="destructive">
|
|
||||||
End session
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// 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.
|
|
||||||
|
|
||||||
// @vitest-environment happy-dom
|
|
||||||
|
|
||||||
import { parseISO } from "date-fns";
|
|
||||||
import { create } from "react-test-renderer";
|
|
||||||
import { describe, expect, it, beforeAll } from "vitest";
|
|
||||||
|
|
||||||
import { mockLocale } from "../../test-utils/mockLocale";
|
|
||||||
import { DummyRouter } from "../../test-utils/router";
|
|
||||||
|
|
||||||
import Session from "./Session";
|
|
||||||
|
|
||||||
describe("<Session />", () => {
|
|
||||||
const defaultProps = {
|
|
||||||
id: "session-id",
|
|
||||||
createdAt: parseISO("2023-06-29T03:35:17.451292+00:00"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const finishedAt = parseISO("2023-06-29T03:35:19.451292+00:00");
|
|
||||||
|
|
||||||
beforeAll(() => mockLocale());
|
|
||||||
|
|
||||||
it("renders an active session", () => {
|
|
||||||
const component = create(
|
|
||||||
<DummyRouter>
|
|
||||||
<Session {...defaultProps} />
|
|
||||||
</DummyRouter>,
|
|
||||||
);
|
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a finished session", () => {
|
|
||||||
const component = create(
|
|
||||||
<DummyRouter>
|
|
||||||
<Session {...defaultProps} finishedAt={finishedAt} />
|
|
||||||
</DummyRouter>,
|
|
||||||
);
|
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses session name when truthy", () => {
|
|
||||||
const name = "test session name";
|
|
||||||
const component = create(
|
|
||||||
<DummyRouter>
|
|
||||||
<Session {...defaultProps} finishedAt={finishedAt} name={name} />
|
|
||||||
</DummyRouter>,
|
|
||||||
);
|
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses client name when truthy", () => {
|
|
||||||
const clientName = "Element";
|
|
||||||
const component = create(
|
|
||||||
<DummyRouter>
|
|
||||||
<Session
|
|
||||||
{...defaultProps}
|
|
||||||
finishedAt={finishedAt}
|
|
||||||
clientName={clientName}
|
|
||||||
clientLogoUri="https://client.org/logo.png"
|
|
||||||
/>
|
|
||||||
</DummyRouter>,
|
|
||||||
);
|
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders ip address", () => {
|
|
||||||
const clientName = "Element";
|
|
||||||
const component = create(
|
|
||||||
<DummyRouter>
|
|
||||||
<Session
|
|
||||||
{...defaultProps}
|
|
||||||
finishedAt={finishedAt}
|
|
||||||
clientName={clientName}
|
|
||||||
lastActiveIp="127.0.0.1"
|
|
||||||
/>
|
|
||||||
</DummyRouter>,
|
|
||||||
);
|
|
||||||
expect(component.toJSON()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// 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 { Link } from "@tanstack/react-router";
|
|
||||||
import { H6, Text, Badge } from "@vector-im/compound-web";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { DeviceType } from "../../gql/graphql";
|
|
||||||
import Block from "../Block";
|
|
||||||
import DateTime from "../DateTime";
|
|
||||||
|
|
||||||
import ClientAvatar from "./ClientAvatar";
|
|
||||||
import DeviceTypeIcon from "./DeviceTypeIcon";
|
|
||||||
import LastActive from "./LastActive";
|
|
||||||
import styles from "./Session.module.css";
|
|
||||||
|
|
||||||
const SessionMetadata: React.FC<React.ComponentProps<typeof Text>> = (
|
|
||||||
props,
|
|
||||||
) => <Text {...props} size="sm" className={styles.sessionMetadata} />;
|
|
||||||
|
|
||||||
type SessionProps = {
|
|
||||||
id: string;
|
|
||||||
name?: string;
|
|
||||||
createdAt: Date;
|
|
||||||
finishedAt?: Date;
|
|
||||||
clientName?: string;
|
|
||||||
clientLogoUri?: string;
|
|
||||||
isCurrent?: boolean;
|
|
||||||
deviceType?: DeviceType;
|
|
||||||
lastActiveIp?: string;
|
|
||||||
lastActiveAt?: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Session: React.FC<React.PropsWithChildren<SessionProps>> = ({
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
createdAt,
|
|
||||||
finishedAt,
|
|
||||||
clientName,
|
|
||||||
clientLogoUri,
|
|
||||||
lastActiveIp,
|
|
||||||
lastActiveAt,
|
|
||||||
isCurrent,
|
|
||||||
children,
|
|
||||||
deviceType,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Block className={styles.session}>
|
|
||||||
<DeviceTypeIcon deviceType={deviceType || DeviceType.Unknown} />
|
|
||||||
<div className={styles.container}>
|
|
||||||
{isCurrent && (
|
|
||||||
<Badge kind="success">{t("frontend.session.current_badge")}</Badge>
|
|
||||||
)}
|
|
||||||
<H6 className={styles.sessionName} title={id}>
|
|
||||||
<Link to="/sessions/$id" params={{ id }}>
|
|
||||||
{name || id}
|
|
||||||
</Link>
|
|
||||||
</H6>
|
|
||||||
<SessionMetadata weight="semibold">
|
|
||||||
<Trans
|
|
||||||
i18nKey="frontend.session.signed_in_date"
|
|
||||||
components={{ datetime: <DateTime datetime={createdAt} /> }}
|
|
||||||
/>
|
|
||||||
</SessionMetadata>
|
|
||||||
{!!finishedAt && (
|
|
||||||
<SessionMetadata weight="semibold" data-finished={true}>
|
|
||||||
<Trans
|
|
||||||
i18nKey="frontend.session.finished_date"
|
|
||||||
components={{ datetime: <DateTime datetime={finishedAt} /> }}
|
|
||||||
/>
|
|
||||||
</SessionMetadata>
|
|
||||||
)}
|
|
||||||
{!!lastActiveAt && (
|
|
||||||
<SessionMetadata>
|
|
||||||
<LastActive lastActive={lastActiveAt} />
|
|
||||||
</SessionMetadata>
|
|
||||||
)}
|
|
||||||
{!!lastActiveIp && <SessionMetadata>{lastActiveIp}</SessionMetadata>}
|
|
||||||
{!!clientName && (
|
|
||||||
<SessionMetadata>
|
|
||||||
<ClientAvatar
|
|
||||||
size="var(--cpd-space-4x)"
|
|
||||||
name={clientName}
|
|
||||||
logoUri={clientLogoUri}
|
|
||||||
/>{" "}
|
|
||||||
<SessionMetadata weight="semibold" as="span">
|
|
||||||
{clientName}
|
|
||||||
</SessionMetadata>
|
|
||||||
</SessionMetadata>
|
|
||||||
)}
|
|
||||||
{!!children && <div className={styles.sessionActions}>{children}</div>}
|
|
||||||
</div>
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Session;
|
|
||||||
165
frontend/src/components/SessionCard/SessionCard.module.css
Normal file
165
frontend/src/components/SessionCard/SessionCard.module.css
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/* Copyright 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.
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.root:has(.action) .body .header {
|
||||||
|
/* We can't exactly know the width of the action button,
|
||||||
|
* so we use a somewhat arbitrary safe value:
|
||||||
|
* - the button padding is 4x + 5x
|
||||||
|
* - the icon is 5x wide
|
||||||
|
* - a 2x safe margin
|
||||||
|
* - the approximate width of the button text "Sign out"
|
||||||
|
*
|
||||||
|
* Of course that depends on the translation, but it's a good start
|
||||||
|
*/
|
||||||
|
padding-inline-end: calc(var(--cpd-space-16x) + 10ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.root:has(.action) .body {
|
||||||
|
/* On small screen, the action button is at the bottom, and we can accurately
|
||||||
|
* calculate the height of the button:
|
||||||
|
*
|
||||||
|
* - the button padding is 1x + 1x
|
||||||
|
* - its line height is 6.5x
|
||||||
|
* - plus 2x 1px of border (so 0.5x)
|
||||||
|
* - the 4x margin on top of the button
|
||||||
|
* - the regular 6x padding
|
||||||
|
*/
|
||||||
|
padding-block-end: calc(var(--cpd-space-9x) + var(--cpd-space-6x) + var(--cpd-space-6x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border-radius: var(--cpd-space-4x);
|
||||||
|
background-color: var(--cpd-color-bg-canvas-default);
|
||||||
|
outline: 1px solid var(--cpd-color-border-interactive-secondary);
|
||||||
|
outline-offset: -1px;
|
||||||
|
box-shadow: 0px 1.2px 2.4px 0px rgba(0, 0, 0, 0.15);
|
||||||
|
padding: var(--cpd-space-6x);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
outline-color: var(--cpd-color-border-disabled);
|
||||||
|
background-color: var(--cpd-color-bg-canvas-disabled);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: var(--cpd-space-3x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.body:not(.disabled) {
|
||||||
|
transition-property: outline-color, box-shadow;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
transition-timing-function: linear;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible {
|
||||||
|
box-shadow: none;
|
||||||
|
outline: 2px solid var(--cpd-color-border-interactive-hovered);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline-color: var(--cpd-color-border-focused);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.action {
|
||||||
|
position: absolute;
|
||||||
|
/* This padding creates a safe area for the action button */
|
||||||
|
padding: var(--cpd-space-6x) var(--cpd-space-6x) var(--cpd-space-2x) var(--cpd-space-2x);
|
||||||
|
inset-block-start: 0;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.action {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
padding: var(--cpd-space-6x);
|
||||||
|
inset-block-end: 0;
|
||||||
|
inset-inline: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--cpd-space-4x);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& .content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* This makes sure it can shrink, and that the text doesn't overflow */
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
& .name {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font: var(--cpd-font-body-md-semibold);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||||
|
color: var(--cpd-color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .client {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
|
||||||
|
& img {
|
||||||
|
margin-inline-end: var(--cpd-space-1x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--cpd-space-4x) var(--cpd-space-10x);
|
||||||
|
|
||||||
|
& .key {
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-sm);
|
||||||
|
color: var(--cpd-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .value {
|
||||||
|
font: var(--cpd-font-body-md-regular);
|
||||||
|
letter-spacing: var(--cpd-font-letter-spacing-body-md);
|
||||||
|
color: var(--cpd-color-text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
frontend/src/components/SessionCard/SessionCard.stories.tsx
Normal file
75
frontend/src/components/SessionCard/SessionCard.stories.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 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.
|
||||||
|
// 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 type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import IconSignOut from "@vector-im/compound-design-tokens/icons/sign-out.svg?react";
|
||||||
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { DeviceType } from "../../gql/graphql";
|
||||||
|
|
||||||
|
import * as Card from "./SessionCard";
|
||||||
|
|
||||||
|
const Template: React.FC<{
|
||||||
|
deviceType: DeviceType;
|
||||||
|
deviceName: string;
|
||||||
|
clientName?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}> = ({ deviceType, deviceName, clientName, disabled }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Body disabled={disabled}>
|
||||||
|
<Card.Header type={deviceType}>
|
||||||
|
<Card.Name name={deviceName} />
|
||||||
|
{clientName && <Card.Client name={clientName} />}
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Metadata>
|
||||||
|
<Card.Info label="Last active">2 hours ago</Card.Info>
|
||||||
|
<Card.Info label="Signed in">NOV 30, 2023</Card.Info>
|
||||||
|
<Card.Info label="Device ID">XXXXXX</Card.Info>
|
||||||
|
</Card.Metadata>
|
||||||
|
</Card.Body>
|
||||||
|
{!disabled && (
|
||||||
|
<Card.Action>
|
||||||
|
<Button kind="secondary" destructive size="sm" Icon={IconSignOut}>
|
||||||
|
{t("frontend.end_session_button.text")}
|
||||||
|
</Button>
|
||||||
|
</Card.Action>
|
||||||
|
)}
|
||||||
|
</Card.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "UI/Session/Card",
|
||||||
|
component: Template,
|
||||||
|
args: {
|
||||||
|
disabled: false,
|
||||||
|
deviceName: "MacBook Pro 16",
|
||||||
|
clientName: "Firefox",
|
||||||
|
deviceType: DeviceType.Pc,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
deviceType: { control: "select", options: Object.values(DeviceType) },
|
||||||
|
disabled: { control: "boolean" },
|
||||||
|
deviceName: { control: "text" },
|
||||||
|
clientName: { control: "text" },
|
||||||
|
},
|
||||||
|
} satisfies Meta<typeof Template>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Template>;
|
||||||
|
|
||||||
|
export const Basic: Story = {};
|
||||||
101
frontend/src/components/SessionCard/SessionCard.tsx
Normal file
101
frontend/src/components/SessionCard/SessionCard.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
// Copyright 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.
|
||||||
|
// 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 { LinkComponent, useLinkProps } from "@tanstack/react-router";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { forwardRef } from "react";
|
||||||
|
|
||||||
|
import { DeviceType } from "../../gql/graphql";
|
||||||
|
import ClientAvatar from "../Session/ClientAvatar";
|
||||||
|
import DeviceTypeIcon from "../Session/DeviceTypeIcon";
|
||||||
|
|
||||||
|
import styles from "./SessionCard.module.css";
|
||||||
|
|
||||||
|
export const Root: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||||
|
<section className={styles.root}>{children}</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
type BodyProps = React.PropsWithChildren<{
|
||||||
|
disabled?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
|
}>;
|
||||||
|
export const LinkBody: LinkComponent = forwardRef<
|
||||||
|
HTMLAnchorElement,
|
||||||
|
Parameters<typeof useLinkProps>[0] & BodyProps
|
||||||
|
>(({ children, disabled, compact, ...props }, ref) => {
|
||||||
|
const linkProps = useLinkProps({
|
||||||
|
className: cx(
|
||||||
|
styles.body,
|
||||||
|
compact && styles.compact,
|
||||||
|
disabled && styles.disabled,
|
||||||
|
),
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<a ref={ref} {...linkProps}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}) as LinkComponent;
|
||||||
|
|
||||||
|
export const Body: React.FC<BodyProps> = ({ children, compact, disabled }) => (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
styles.body,
|
||||||
|
compact && styles.compact,
|
||||||
|
disabled && styles.disabled,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type HeaderProps = React.PropsWithChildren<{ type: DeviceType }>;
|
||||||
|
export const Header: React.FC<HeaderProps> = ({ type, children }) => (
|
||||||
|
<header className={styles.header}>
|
||||||
|
<DeviceTypeIcon deviceType={type} />
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
|
||||||
|
type NameProps = { name: string };
|
||||||
|
export const Name: React.FC<NameProps> = ({ name }) => (
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type ClientProps = { name: string; logoUri?: string };
|
||||||
|
export const Client: React.FC<ClientProps> = ({ name, logoUri }) => (
|
||||||
|
<div className={styles.client}>
|
||||||
|
<ClientAvatar name={name} size="var(--cpd-space-5x)" logoUri={logoUri} />
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Metadata: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||||
|
<ul className={styles.metadata}>{children}</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Info: React.FC<React.PropsWithChildren<{ label: string }>> = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
}) => (
|
||||||
|
<li>
|
||||||
|
<div className={styles.key}>{label}</div>
|
||||||
|
<div className={styles.value}>{children}</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Action: React.FC<React.PropsWithChildren> = ({ children }) => (
|
||||||
|
<div className={styles.action}>{children}</div>
|
||||||
|
);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
@@ -12,4 +12,14 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
export { default as Session } from "./Session";
|
export {
|
||||||
|
Root,
|
||||||
|
LinkBody,
|
||||||
|
Body,
|
||||||
|
Header,
|
||||||
|
Name,
|
||||||
|
Client,
|
||||||
|
Metadata,
|
||||||
|
Info,
|
||||||
|
Action,
|
||||||
|
} from "./SessionCard";
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<CompatSession /> > renders a finished session 1`] = `
|
exports[`<CompatSession /> > renders a finished session 1`] = `
|
||||||
<div
|
<section
|
||||||
className="_block_17898c _session_634806"
|
className="_root_e2909e"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="_body_e2909e _disabled_e2909e"
|
||||||
|
href="/sessions/session-id"
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="_header_e2909e"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Unknown device type"
|
aria-label="Unknown device type"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
@@ -21,68 +34,73 @@ exports[`<CompatSession /> > renders a finished session 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="_container_634806"
|
className="_content_e2909e"
|
||||||
>
|
>
|
||||||
<h6
|
<div
|
||||||
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
|
className="_name_e2909e"
|
||||||
title="session-id"
|
|
||||||
>
|
>
|
||||||
<a
|
Unknown device
|
||||||
href="/sessions/session-id"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
onFocus={[Function]}
|
className="_client_e2909e"
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
style={{}}
|
|
||||||
>
|
>
|
||||||
abcd1234
|
element.io
|
||||||
</a>
|
</div>
|
||||||
</h6>
|
</div>
|
||||||
<p
|
</header>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
<ul
|
||||||
|
className="_metadata_e2909e"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
>
|
>
|
||||||
Signed in
|
Signed in
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
<time
|
<time
|
||||||
dateTime="2023-06-29T03:35:17Z"
|
dateTime="2023-06-29T03:35:17Z"
|
||||||
>
|
>
|
||||||
Thu, 29 Jun 2023, 03:35
|
Thu, 29 Jun 2023, 03:35
|
||||||
</time>
|
</time>
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
data-finished={true}
|
|
||||||
>
|
|
||||||
Finished
|
|
||||||
<time
|
|
||||||
dateTime="2023-06-29T03:35:19Z"
|
|
||||||
>
|
|
||||||
Thu, 29 Jun 2023, 03:35
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
1.2.3.4
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
element.io
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
|
>
|
||||||
|
Device ID
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
|
abcd1234
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<CompatSession /> > renders an active session 1`] = `
|
exports[`<CompatSession /> > renders an active session 1`] = `
|
||||||
<div
|
<section
|
||||||
className="_block_17898c _session_634806"
|
className="_root_e2909e"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="_body_e2909e"
|
||||||
|
href="/sessions/session-id"
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="_header_e2909e"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Unknown device type"
|
aria-label="Unknown device type"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
@@ -100,51 +118,55 @@ exports[`<CompatSession /> > renders an active session 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="_container_634806"
|
className="_content_e2909e"
|
||||||
>
|
>
|
||||||
<h6
|
<div
|
||||||
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
|
className="_name_e2909e"
|
||||||
title="session-id"
|
|
||||||
>
|
>
|
||||||
<a
|
Unknown device
|
||||||
href="/sessions/session-id"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
onFocus={[Function]}
|
className="_client_e2909e"
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
style={{}}
|
|
||||||
>
|
>
|
||||||
abcd1234
|
element.io
|
||||||
</a>
|
</div>
|
||||||
</h6>
|
</div>
|
||||||
<p
|
</header>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
<ul
|
||||||
|
className="_metadata_e2909e"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
>
|
>
|
||||||
Signed in
|
Signed in
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
<time
|
<time
|
||||||
dateTime="2023-06-29T03:35:17Z"
|
dateTime="2023-06-29T03:35:17Z"
|
||||||
>
|
>
|
||||||
Thu, 29 Jun 2023, 03:35
|
Thu, 29 Jun 2023, 03:35
|
||||||
</time>
|
</time>
|
||||||
</p>
|
</div>
|
||||||
<p
|
</li>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
<li>
|
||||||
>
|
|
||||||
1.2.3.4
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
element.io
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
className="_sessionActions_634806"
|
className="_key_e2909e"
|
||||||
|
>
|
||||||
|
Device ID
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
|
abcd1234
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
className="_action_e2909e"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-controls="radix-:r0:"
|
aria-controls="radix-:r0:"
|
||||||
@@ -174,6 +196,5 @@ exports[`<CompatSession /> > renders an active session 1`] = `
|
|||||||
Sign out
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`<OAuth2Session /> > renders a finished session 1`] = `
|
exports[`<OAuth2Session /> > renders a finished session 1`] = `
|
||||||
<div
|
<section
|
||||||
className="_block_17898c _session_634806"
|
className="_root_e2909e"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="_body_e2909e _disabled_e2909e"
|
||||||
|
href="/sessions/session-id"
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="_header_e2909e"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Computer"
|
aria-label="Computer"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
@@ -18,68 +31,73 @@ exports[`<OAuth2Session /> > renders a finished session 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="_container_634806"
|
className="_content_e2909e"
|
||||||
>
|
>
|
||||||
<h6
|
<div
|
||||||
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
|
className="_name_e2909e"
|
||||||
title="session-id"
|
|
||||||
>
|
>
|
||||||
<a
|
Unknown device
|
||||||
href="/sessions/session-id"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
onFocus={[Function]}
|
className="_client_e2909e"
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
style={{}}
|
|
||||||
>
|
>
|
||||||
abcd1234
|
Element
|
||||||
</a>
|
</div>
|
||||||
</h6>
|
</div>
|
||||||
<p
|
</header>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
<ul
|
||||||
|
className="_metadata_e2909e"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
>
|
>
|
||||||
Signed in
|
Signed in
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
<time
|
<time
|
||||||
dateTime="2023-06-29T03:35:17Z"
|
dateTime="2023-06-29T03:35:17Z"
|
||||||
>
|
>
|
||||||
Thu, 29 Jun 2023, 03:35
|
Thu, 29 Jun 2023, 03:35
|
||||||
</time>
|
</time>
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
data-finished={true}
|
|
||||||
>
|
|
||||||
Finished
|
|
||||||
<time
|
|
||||||
dateTime="2023-06-29T03:35:19Z"
|
|
||||||
>
|
|
||||||
Thu, 29 Jun 2023, 03:35
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
1.2.3.4
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
Element
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
|
>
|
||||||
|
Device ID
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
|
abcd1234
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<OAuth2Session /> > renders an active session 1`] = `
|
exports[`<OAuth2Session /> > renders an active session 1`] = `
|
||||||
<div
|
<section
|
||||||
className="_block_17898c _session_634806"
|
className="_root_e2909e"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="_body_e2909e"
|
||||||
|
href="/sessions/session-id"
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="_header_e2909e"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Computer"
|
aria-label="Computer"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
@@ -94,51 +112,55 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="_container_634806"
|
className="_content_e2909e"
|
||||||
>
|
>
|
||||||
<h6
|
<div
|
||||||
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
|
className="_name_e2909e"
|
||||||
title="session-id"
|
|
||||||
>
|
>
|
||||||
<a
|
Unknown device
|
||||||
href="/sessions/session-id"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
onFocus={[Function]}
|
className="_client_e2909e"
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
style={{}}
|
|
||||||
>
|
>
|
||||||
abcd1234
|
Element
|
||||||
</a>
|
</div>
|
||||||
</h6>
|
</div>
|
||||||
<p
|
</header>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
<ul
|
||||||
|
className="_metadata_e2909e"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
>
|
>
|
||||||
Signed in
|
Signed in
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
<time
|
<time
|
||||||
dateTime="2023-06-29T03:35:17Z"
|
dateTime="2023-06-29T03:35:17Z"
|
||||||
>
|
>
|
||||||
Thu, 29 Jun 2023, 03:35
|
Thu, 29 Jun 2023, 03:35
|
||||||
</time>
|
</time>
|
||||||
</p>
|
</div>
|
||||||
<p
|
</li>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
<li>
|
||||||
>
|
|
||||||
1.2.3.4
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
Element
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div
|
<div
|
||||||
className="_sessionActions_634806"
|
className="_key_e2909e"
|
||||||
|
>
|
||||||
|
Device ID
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
|
abcd1234
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
className="_action_e2909e"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-controls="radix-:r0:"
|
aria-controls="radix-:r0:"
|
||||||
@@ -168,14 +190,26 @@ exports[`<OAuth2Session /> > renders an active session 1`] = `
|
|||||||
Sign out
|
Sign out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`<OAuth2Session /> > renders correct icon for a native session 1`] = `
|
exports[`<OAuth2Session /> > renders correct icon for a native session 1`] = `
|
||||||
<div
|
<section
|
||||||
className="_block_17898c _session_634806"
|
className="_root_e2909e"
|
||||||
>
|
>
|
||||||
|
<a
|
||||||
|
className="_body_e2909e _disabled_e2909e"
|
||||||
|
href="/sessions/session-id"
|
||||||
|
onClick={[Function]}
|
||||||
|
onFocus={[Function]}
|
||||||
|
onMouseEnter={[Function]}
|
||||||
|
onMouseLeave={[Function]}
|
||||||
|
onTouchStart={[Function]}
|
||||||
|
style={{}}
|
||||||
|
>
|
||||||
|
<header
|
||||||
|
className="_header_e2909e"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-label="Mobile"
|
aria-label="Mobile"
|
||||||
className="_icon_e677aa"
|
className="_icon_e677aa"
|
||||||
@@ -190,60 +224,52 @@ exports[`<OAuth2Session /> > renders correct icon for a native session 1`] = `
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
<div
|
||||||
className="_container_634806"
|
className="_content_e2909e"
|
||||||
>
|
>
|
||||||
<h6
|
<div
|
||||||
className="_typography_yh5dq_162 _font-body-md-semibold_yh5dq_64 _sessionName_634806"
|
className="_name_e2909e"
|
||||||
title="session-id"
|
|
||||||
>
|
>
|
||||||
<a
|
Unknown device
|
||||||
href="/sessions/session-id"
|
</div>
|
||||||
onClick={[Function]}
|
<div
|
||||||
onFocus={[Function]}
|
className="_client_e2909e"
|
||||||
onMouseEnter={[Function]}
|
|
||||||
onMouseLeave={[Function]}
|
|
||||||
onTouchStart={[Function]}
|
|
||||||
style={{}}
|
|
||||||
>
|
>
|
||||||
abcd1234
|
Element
|
||||||
</a>
|
</div>
|
||||||
</h6>
|
</div>
|
||||||
<p
|
</header>
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
<ul
|
||||||
|
className="_metadata_e2909e"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
>
|
>
|
||||||
Signed in
|
Signed in
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
<time
|
<time
|
||||||
dateTime="2023-06-29T03:35:17Z"
|
dateTime="2023-06-29T03:35:17Z"
|
||||||
>
|
>
|
||||||
Thu, 29 Jun 2023, 03:35
|
Thu, 29 Jun 2023, 03:35
|
||||||
</time>
|
</time>
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
data-finished={true}
|
|
||||||
>
|
|
||||||
Finished
|
|
||||||
<time
|
|
||||||
dateTime="2023-06-29T03:35:19Z"
|
|
||||||
>
|
|
||||||
Thu, 29 Jun 2023, 03:35
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
1.2.3.4
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-regular_yh5dq_40 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
className="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _sessionMetadata_634806"
|
|
||||||
>
|
|
||||||
Element
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
className="_key_e2909e"
|
||||||
|
>
|
||||||
|
Device ID
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="_value_e2909e"
|
||||||
|
>
|
||||||
|
abcd1234
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ const documents = {
|
|||||||
types.EndBrowserSessionDocument,
|
types.EndBrowserSessionDocument,
|
||||||
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
|
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
|
||||||
types.OAuth2Client_DetailFragmentDoc,
|
types.OAuth2Client_DetailFragmentDoc,
|
||||||
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n":
|
||||||
types.CompatSession_SessionFragmentDoc,
|
types.CompatSession_SessionFragmentDoc,
|
||||||
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
|
||||||
types.EndCompatSessionDocument,
|
types.EndCompatSessionDocument,
|
||||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
|
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n":
|
||||||
types.OAuth2Session_SessionFragmentDoc,
|
types.OAuth2Session_SessionFragmentDoc,
|
||||||
"\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n":
|
"\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,
|
types.EndOAuth2SessionDocument,
|
||||||
@@ -119,8 +119,8 @@ export function graphql(
|
|||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
source: "\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
|
): (typeof documents)["\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
@@ -131,8 +131,8 @@ export function graphql(
|
|||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
export function graphql(
|
export function graphql(
|
||||||
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
|
source: "\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n",
|
||||||
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n model\n os\n osVersion\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"];
|
): (typeof documents)["\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"];
|
||||||
/**
|
/**
|
||||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1237,7 +1237,6 @@ export type CompatSession_SessionFragment = {
|
|||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
userAgent?: {
|
userAgent?: {
|
||||||
__typename?: "UserAgent";
|
__typename?: "UserAgent";
|
||||||
raw: string;
|
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
os?: string | null;
|
os?: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
@@ -1277,9 +1276,9 @@ export type OAuth2Session_SessionFragment = {
|
|||||||
lastActiveAt?: string | null;
|
lastActiveAt?: string | null;
|
||||||
userAgent?: {
|
userAgent?: {
|
||||||
__typename?: "UserAgent";
|
__typename?: "UserAgent";
|
||||||
|
name?: string | null;
|
||||||
model?: string | null;
|
model?: string | null;
|
||||||
os?: string | null;
|
os?: string | null;
|
||||||
osVersion?: string | null;
|
|
||||||
deviceType: DeviceType;
|
deviceType: DeviceType;
|
||||||
} | null;
|
} | null;
|
||||||
client: {
|
client: {
|
||||||
@@ -1897,7 +1896,6 @@ export const CompatSession_SessionFragmentDoc = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
@@ -1946,9 +1944,9 @@ export const OAuth2Session_SessionFragmentDoc = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -2573,9 +2571,9 @@ export const EndOAuth2SessionDocument = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -4233,7 +4231,6 @@ export const AppSessionsListQueryDocument = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
{ kind: "Field", name: { kind: "Name", value: "raw" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
@@ -4277,9 +4274,9 @@ export const AppSessionsListQueryDocument = {
|
|||||||
selectionSet: {
|
selectionSet: {
|
||||||
kind: "SelectionSet",
|
kind: "SelectionSet",
|
||||||
selections: [
|
selections: [
|
||||||
|
{ kind: "Field", name: { kind: "Name", value: "name" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
{ kind: "Field", name: { kind: "Name", value: "model" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
{ kind: "Field", name: { kind: "Name", value: "os" } },
|
||||||
{ kind: "Field", name: { kind: "Name", value: "osVersion" } },
|
|
||||||
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
{ kind: "Field", name: { kind: "Name", value: "deviceType" } },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user