Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit bf13e58

Browse files
Kerry Archibaldsandhose
authored andcommitted
link to client detail, design pass on client detail page
1 parent 5e76adb commit bf13e58

File tree

10 files changed

+251
-41
lines changed

10 files changed

+251
-41
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* Copyright 2023 The Matrix.org Foundation C.I.C.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
.header {
17+
display: flex;
18+
flex-direction: row;
19+
justify-content: flex-start;
20+
align-items: center;
21+
gap: var(--cpd-space-2x);
22+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2023 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { H3 } from "@vector-im/compound-web";
16+
17+
import { FragmentType, useFragment } from "../../gql";
18+
import { graphql } from "../../gql/gql";
19+
import BlockList from "../BlockList/BlockList";
20+
import ExternalLink from "../ExternalLink/ExternalLink";
21+
import ClientAvatar from "../Session/ClientAvatar";
22+
import SessionDetails from "../SessionDetail/SessionDetails";
23+
24+
import styles from "./OAuth2ClientDetail.module.css";
25+
26+
export const OAUTH2_CLIENT_FRAGMENT = graphql(/* GraphQL */ `
27+
fragment OAuth2Client_detail on Oauth2Client {
28+
id
29+
clientId
30+
clientName
31+
clientUri
32+
logoUri
33+
tosUri
34+
policyUri
35+
redirectUris
36+
}
37+
`);
38+
39+
type Props = {
40+
client: FragmentType<typeof OAUTH2_CLIENT_FRAGMENT>;
41+
};
42+
43+
const FriendlyExternalLink: React.FC<{ uri?: string }> = ({ uri }) => {
44+
if (!uri) {
45+
return null;
46+
}
47+
const url = new URL(uri);
48+
const friendlyUrl = url.host + url.pathname;
49+
50+
return <ExternalLink href={uri}>{friendlyUrl}</ExternalLink>;
51+
};
52+
53+
const OAuth2ClientDetail: React.FC<Props> = ({ client }) => {
54+
const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client);
55+
56+
const details = [
57+
{ label: "Name", value: data.clientName },
58+
{ label: "Client ID", value: <code>{data.clientId}</code> },
59+
{
60+
label: "Terms of service",
61+
value: data.tosUri && <FriendlyExternalLink uri={data.tosUri} />,
62+
},
63+
{
64+
label: "Policy",
65+
value: data.policyUri && <FriendlyExternalLink uri={data.policyUri} />,
66+
},
67+
].filter(({ value }) => !!value);
68+
69+
return (
70+
<BlockList>
71+
<header className={styles.header}>
72+
<ClientAvatar
73+
logoUri={data.logoUri}
74+
name={data.clientName || data.clientId}
75+
size="1.5rem"
76+
/>
77+
<H3>{data.clientName}</H3>
78+
</header>
79+
<SessionDetails title="Client" details={details} />
80+
</BlockList>
81+
);
82+
};
83+
84+
export default OAuth2ClientDetail;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* Copyright 2023 The Matrix.org Foundation C.I.C.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
.external-link {
17+
/* override compound style */
18+
color: var(--cpd-color-text-link-external) !important;
19+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2023 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Link } from "@vector-im/compound-web";
16+
import classNames from "classnames";
17+
18+
import styles from "./ExternalLink.module.css";
19+
20+
const ExternalLink: React.FC<React.ComponentProps<typeof Link>> = ({
21+
children,
22+
className,
23+
...props
24+
}) => (
25+
<Link
26+
className={classNames(className, styles.externalLink)}
27+
target="_blank"
28+
{...props}
29+
>
30+
{children}
31+
</Link>
32+
);
33+
34+
export default ExternalLink;

frontend/src/components/SessionDetail/CompatSessionDetail.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
simplifyUrl,
2424
} from "../CompatSession";
2525
import DateTime from "../DateTime";
26+
import ExternalLink from "../ExternalLink/ExternalLink";
2627
import EndSessionButton from "../Session/EndSessionButton";
2728

2829
import SessionDetails from "./SessionDetails";
@@ -59,9 +60,9 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
5960
clientDetails.push({
6061
label: "Uri",
6162
value: (
62-
<a target="_blank" href={data.ssoLogin?.redirectUri}>
63+
<ExternalLink target="_blank" href={data.ssoLogin?.redirectUri}>
6364
{data.ssoLogin?.redirectUri}
64-
</a>
65+
</ExternalLink>
6566
),
6667
});
6768
}

frontend/src/components/SessionDetail/OAuth2SessionDetail.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { H3 } from "@vector-im/compound-web";
1616
import { useSetAtom } from "jotai";
1717

1818
import { FragmentType, useFragment } from "../../gql";
19+
import { Link } from "../../routing";
1920
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
2021
import BlockList from "../BlockList/BlockList";
2122
import DateTime from "../DateTime";
@@ -70,6 +71,9 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
7071
},
7172
];
7273

74+
const clientTitle = (
75+
<Link route={{ type: "client", id: data.client.id }}>Client</Link>
76+
);
7377
const clientDetails = [
7478
{
7579
label: "Name",
@@ -100,7 +104,7 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
100104
<BlockList>
101105
<H3>{deviceId || data.id}</H3>
102106
<SessionDetails title="Session" details={sessionDetails} />
103-
<SessionDetails title="Client" details={clientDetails} />
107+
<SessionDetails title={clientTitle} details={clientDetails} />
104108
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
105109
</BlockList>
106110
</div>

frontend/src/components/SessionDetail/SessionDetails.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import styles from "./SessionDetails.module.css";
2121

2222
type Detail = { label: string; value: string | ReactNode };
2323
type Props = {
24-
title: string;
24+
title: string | ReactNode;
2525
details: Detail[];
2626
};
2727

frontend/src/gql/gql.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const documents = {
2323
types.EndBrowserSessionDocument,
2424
"\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: $state\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":
2525
types.BrowserSessionListDocument,
26+
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n":
27+
types.OAuth2Client_DetailFragmentDoc,
2628
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
2729
types.CompatSession_SessionFragmentDoc,
2830
"\n mutation EndCompatSession($id: ID!) {\n endCompatSession(input: { compatSessionId: $id }) {\n status\n compatSession {\n id\n finishedAt\n }\n }\n }\n":
@@ -65,7 +67,7 @@ const documents = {
6567
types.ResendVerificationEmailDocument,
6668
"\n query BrowserSessionQuery($id: ID!) {\n browserSession(id: $id) {\n id\n createdAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n }\n":
6769
types.BrowserSessionQueryDocument,
68-
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n":
70+
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
6971
types.OAuth2ClientQueryDocument,
7072
"\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserSessionsOverview_user\n }\n }\n }\n":
7173
types.SessionsOverviewQueryDocument,
@@ -117,6 +119,12 @@ export function graphql(
117119
export function graphql(
118120
source: "\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: $state\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",
119121
): (typeof documents)["\n query BrowserSessionList(\n $userId: ID!\n $state: BrowserSessionState\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: $state\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"];
122+
/**
123+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
124+
*/
125+
export function graphql(
126+
source: "\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n",
127+
): (typeof documents)["\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n"];
120128
/**
121129
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
122130
*/
@@ -247,8 +255,8 @@ export function graphql(
247255
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
248256
*/
249257
export function graphql(
250-
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n",
251-
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n id\n clientId\n clientName\n clientUri\n tosUri\n policyUri\n redirectUris\n logoUri\n }\n }\n"];
258+
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n",
259+
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n"];
252260
/**
253261
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
254262
*/

frontend/src/gql/graphql.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,18 @@ export type BrowserSessionListQuery = {
10831083
} | null;
10841084
};
10851085

1086+
export type OAuth2Client_DetailFragment = {
1087+
__typename?: "Oauth2Client";
1088+
id: string;
1089+
clientId: string;
1090+
clientName?: string | null;
1091+
clientUri?: any | null;
1092+
logoUri?: any | null;
1093+
tosUri?: any | null;
1094+
policyUri?: any | null;
1095+
redirectUris: Array<any>;
1096+
} & { " $fragmentName"?: "OAuth2Client_DetailFragment" };
1097+
10861098
export type CompatSession_SessionFragment = {
10871099
__typename?: "CompatSession";
10881100
id: string;
@@ -1497,17 +1509,13 @@ export type OAuth2ClientQueryQueryVariables = Exact<{
14971509

14981510
export type OAuth2ClientQueryQuery = {
14991511
__typename?: "Query";
1500-
oauth2Client?: {
1501-
__typename?: "Oauth2Client";
1502-
id: string;
1503-
clientId: string;
1504-
clientName?: string | null;
1505-
clientUri?: any | null;
1506-
tosUri?: any | null;
1507-
policyUri?: any | null;
1508-
redirectUris: Array<any>;
1509-
logoUri?: any | null;
1510-
} | null;
1512+
oauth2Client?:
1513+
| ({ __typename?: "Oauth2Client" } & {
1514+
" $fragmentRefs"?: {
1515+
OAuth2Client_DetailFragment: OAuth2Client_DetailFragment;
1516+
};
1517+
})
1518+
| null;
15111519
};
15121520

15131521
export type SessionsOverviewQueryQueryVariables = Exact<{
@@ -1573,6 +1581,32 @@ export const BrowserSession_SessionFragmentDoc = {
15731581
},
15741582
],
15751583
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
1584+
export const OAuth2Client_DetailFragmentDoc = {
1585+
kind: "Document",
1586+
definitions: [
1587+
{
1588+
kind: "FragmentDefinition",
1589+
name: { kind: "Name", value: "OAuth2Client_detail" },
1590+
typeCondition: {
1591+
kind: "NamedType",
1592+
name: { kind: "Name", value: "Oauth2Client" },
1593+
},
1594+
selectionSet: {
1595+
kind: "SelectionSet",
1596+
selections: [
1597+
{ kind: "Field", name: { kind: "Name", value: "id" } },
1598+
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
1599+
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
1600+
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
1601+
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
1602+
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
1603+
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
1604+
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
1605+
],
1606+
},
1607+
},
1608+
],
1609+
} as unknown as DocumentNode<OAuth2Client_DetailFragment, unknown>;
15761610
export const CompatSession_SessionFragmentDoc = {
15771611
kind: "Document",
15781612
definitions: [
@@ -4166,23 +4200,37 @@ export const OAuth2ClientQueryDocument = {
41664200
selectionSet: {
41674201
kind: "SelectionSet",
41684202
selections: [
4169-
{ kind: "Field", name: { kind: "Name", value: "id" } },
4170-
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
4171-
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
4172-
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
4173-
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
4174-
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
41754203
{
4176-
kind: "Field",
4177-
name: { kind: "Name", value: "redirectUris" },
4204+
kind: "FragmentSpread",
4205+
name: { kind: "Name", value: "OAuth2Client_detail" },
41784206
},
4179-
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
41804207
],
41814208
},
41824209
},
41834210
],
41844211
},
41854212
},
4213+
{
4214+
kind: "FragmentDefinition",
4215+
name: { kind: "Name", value: "OAuth2Client_detail" },
4216+
typeCondition: {
4217+
kind: "NamedType",
4218+
name: { kind: "Name", value: "Oauth2Client" },
4219+
},
4220+
selectionSet: {
4221+
kind: "SelectionSet",
4222+
selections: [
4223+
{ kind: "Field", name: { kind: "Name", value: "id" } },
4224+
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
4225+
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
4226+
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
4227+
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
4228+
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
4229+
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
4230+
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
4231+
],
4232+
},
4233+
},
41864234
],
41874235
} as unknown as DocumentNode<
41884236
OAuth2ClientQueryQuery,

0 commit comments

Comments
 (0)