1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-11-23 11:02:35 +03:00

link to client detail, design pass on client detail page

This commit is contained in:
Kerry Archibald
2023-09-08 14:35:52 +12:00
committed by Quentin Gliech
parent 5e76adb325
commit bf13e58d16
10 changed files with 251 additions and 41 deletions

View File

@@ -0,0 +1,22 @@
/* 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.
*/
.header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--cpd-space-2x);
}

View File

@@ -0,0 +1,84 @@
// 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 { H3 } from "@vector-im/compound-web";
import { FragmentType, useFragment } from "../../gql";
import { graphql } from "../../gql/gql";
import BlockList from "../BlockList/BlockList";
import ExternalLink from "../ExternalLink/ExternalLink";
import ClientAvatar from "../Session/ClientAvatar";
import SessionDetails from "../SessionDetail/SessionDetails";
import styles from "./OAuth2ClientDetail.module.css";
export const OAUTH2_CLIENT_FRAGMENT = graphql(/* GraphQL */ `
fragment OAuth2Client_detail on Oauth2Client {
id
clientId
clientName
clientUri
logoUri
tosUri
policyUri
redirectUris
}
`);
type Props = {
client: FragmentType<typeof OAUTH2_CLIENT_FRAGMENT>;
};
const FriendlyExternalLink: React.FC<{ uri?: string }> = ({ uri }) => {
if (!uri) {
return null;
}
const url = new URL(uri);
const friendlyUrl = url.host + url.pathname;
return <ExternalLink href={uri}>{friendlyUrl}</ExternalLink>;
};
const OAuth2ClientDetail: React.FC<Props> = ({ client }) => {
const data = useFragment(OAUTH2_CLIENT_FRAGMENT, client);
const details = [
{ label: "Name", value: data.clientName },
{ label: "Client ID", value: <code>{data.clientId}</code> },
{
label: "Terms of service",
value: data.tosUri && <FriendlyExternalLink uri={data.tosUri} />,
},
{
label: "Policy",
value: data.policyUri && <FriendlyExternalLink uri={data.policyUri} />,
},
].filter(({ value }) => !!value);
return (
<BlockList>
<header className={styles.header}>
<ClientAvatar
logoUri={data.logoUri}
name={data.clientName || data.clientId}
size="1.5rem"
/>
<H3>{data.clientName}</H3>
</header>
<SessionDetails title="Client" details={details} />
</BlockList>
);
};
export default OAuth2ClientDetail;

View File

@@ -0,0 +1,19 @@
/* 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.
*/
.external-link {
/* override compound style */
color: var(--cpd-color-text-link-external) !important;
}

View File

@@ -0,0 +1,34 @@
// 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 "@vector-im/compound-web";
import classNames from "classnames";
import styles from "./ExternalLink.module.css";
const ExternalLink: React.FC<React.ComponentProps<typeof Link>> = ({
children,
className,
...props
}) => (
<Link
className={classNames(className, styles.externalLink)}
target="_blank"
{...props}
>
{children}
</Link>
);
export default ExternalLink;

View File

@@ -23,6 +23,7 @@ import {
simplifyUrl,
} from "../CompatSession";
import DateTime from "../DateTime";
import ExternalLink from "../ExternalLink/ExternalLink";
import EndSessionButton from "../Session/EndSessionButton";
import SessionDetails from "./SessionDetails";
@@ -59,9 +60,9 @@ const CompatSessionDetail: React.FC<Props> = ({ session }) => {
clientDetails.push({
label: "Uri",
value: (
<a target="_blank" href={data.ssoLogin?.redirectUri}>
<ExternalLink target="_blank" href={data.ssoLogin?.redirectUri}>
{data.ssoLogin?.redirectUri}
</a>
</ExternalLink>
),
});
}

View File

@@ -16,6 +16,7 @@ import { H3 } from "@vector-im/compound-web";
import { useSetAtom } from "jotai";
import { FragmentType, useFragment } from "../../gql";
import { Link } from "../../routing";
import { getDeviceIdFromScope } from "../../utils/deviceIdFromScope";
import BlockList from "../BlockList/BlockList";
import DateTime from "../DateTime";
@@ -70,6 +71,9 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
},
];
const clientTitle = (
<Link route={{ type: "client", id: data.client.id }}>Client</Link>
);
const clientDetails = [
{
label: "Name",
@@ -100,7 +104,7 @@ const OAuth2SessionDetail: React.FC<Props> = ({ session }) => {
<BlockList>
<H3>{deviceId || data.id}</H3>
<SessionDetails title="Session" details={sessionDetails} />
<SessionDetails title="Client" details={clientDetails} />
<SessionDetails title={clientTitle} details={clientDetails} />
{!data.finishedAt && <EndSessionButton endSession={onSessionEnd} />}
</BlockList>
</div>

View File

@@ -21,7 +21,7 @@ import styles from "./SessionDetails.module.css";
type Detail = { label: string; value: string | ReactNode };
type Props = {
title: string;
title: string | ReactNode;
details: Detail[];
};

View File

@@ -23,6 +23,8 @@ const documents = {
types.EndBrowserSessionDocument,
"\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":
types.BrowserSessionListDocument,
"\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,
"\n fragment CompatSession_session on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n ssoLogin {\n id\n redirectUri\n }\n }\n":
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":
@@ -65,7 +67,7 @@ const documents = {
types.ResendVerificationEmailDocument,
"\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":
types.BrowserSessionQueryDocument,
"\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":
"\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n":
types.OAuth2ClientQueryDocument,
"\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...UserSessionsOverview_user\n }\n }\n }\n":
types.SessionsOverviewQueryDocument,
@@ -117,6 +119,12 @@ export function graphql(
export function graphql(
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",
): (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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
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",
): (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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -247,8 +255,8 @@ export function graphql(
* 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 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",
): (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"];
source: "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n",
): (typeof documents)["\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1083,6 +1083,18 @@ export type BrowserSessionListQuery = {
} | null;
};
export type OAuth2Client_DetailFragment = {
__typename?: "Oauth2Client";
id: string;
clientId: string;
clientName?: string | null;
clientUri?: any | null;
logoUri?: any | null;
tosUri?: any | null;
policyUri?: any | null;
redirectUris: Array<any>;
} & { " $fragmentName"?: "OAuth2Client_DetailFragment" };
export type CompatSession_SessionFragment = {
__typename?: "CompatSession";
id: string;
@@ -1497,17 +1509,13 @@ export type OAuth2ClientQueryQueryVariables = Exact<{
export type OAuth2ClientQueryQuery = {
__typename?: "Query";
oauth2Client?: {
__typename?: "Oauth2Client";
id: string;
clientId: string;
clientName?: string | null;
clientUri?: any | null;
tosUri?: any | null;
policyUri?: any | null;
redirectUris: Array<any>;
logoUri?: any | null;
} | null;
oauth2Client?:
| ({ __typename?: "Oauth2Client" } & {
" $fragmentRefs"?: {
OAuth2Client_DetailFragment: OAuth2Client_DetailFragment;
};
})
| null;
};
export type SessionsOverviewQueryQueryVariables = Exact<{
@@ -1573,6 +1581,32 @@ export const BrowserSession_SessionFragmentDoc = {
},
],
} as unknown as DocumentNode<BrowserSession_SessionFragment, unknown>;
export const OAuth2Client_DetailFragmentDoc = {
kind: "Document",
definitions: [
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "OAuth2Client_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "Oauth2Client" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
{ kind: "Field", name: { kind: "Name", value: "id" } },
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
],
},
},
],
} as unknown as DocumentNode<OAuth2Client_DetailFragment, unknown>;
export const CompatSession_SessionFragmentDoc = {
kind: "Document",
definitions: [
@@ -4163,6 +4197,26 @@ export const OAuth2ClientQueryDocument = {
},
},
],
selectionSet: {
kind: "SelectionSet",
selections: [
{
kind: "FragmentSpread",
name: { kind: "Name", value: "OAuth2Client_detail" },
},
],
},
},
],
},
},
{
kind: "FragmentDefinition",
name: { kind: "Name", value: "OAuth2Client_detail" },
typeCondition: {
kind: "NamedType",
name: { kind: "Name", value: "Oauth2Client" },
},
selectionSet: {
kind: "SelectionSet",
selections: [
@@ -4170,16 +4224,10 @@ export const OAuth2ClientQueryDocument = {
{ kind: "Field", name: { kind: "Name", value: "clientId" } },
{ kind: "Field", name: { kind: "Name", value: "clientName" } },
{ kind: "Field", name: { kind: "Name", value: "clientUri" } },
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
{ kind: "Field", name: { kind: "Name", value: "tosUri" } },
{ kind: "Field", name: { kind: "Name", value: "policyUri" } },
{
kind: "Field",
name: { kind: "Name", value: "redirectUris" },
},
{ kind: "Field", name: { kind: "Name", value: "logoUri" } },
],
},
},
{ kind: "Field", name: { kind: "Name", value: "redirectUris" } },
],
},
},

View File

@@ -17,6 +17,7 @@ import { atomFamily } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
import { mapQueryAtom } from "../atoms";
import OAuth2ClientDetail from "../components/Client/OAuth2ClientDetail";
import GraphQLError from "../components/GraphQLError";
import NotFound from "../components/NotFound";
import { graphql } from "../gql";
@@ -25,14 +26,7 @@ import { isErr, unwrapErr, unwrapOk } from "../result";
const QUERY = graphql(/* GraphQL */ `
query OAuth2ClientQuery($id: ID!) {
oauth2Client(id: $id) {
id
clientId
clientName
clientUri
tosUri
policyUri
redirectUris
logoUri
...OAuth2Client_detail
}
}
`);
@@ -56,13 +50,9 @@ const OAuth2Client: React.FC<{ id: string }> = ({ id }) => {
if (isErr(result)) return <GraphQLError error={unwrapErr(result)} />;
const oauth2Client = unwrapOk(result);
if (oauth2Client === null) return <NotFound />;
if (!oauth2Client) return <NotFound />;
return (
<pre>
<code>{JSON.stringify(oauth2Client, null, 2)}</code>
</pre>
);
return <OAuth2ClientDetail client={oauth2Client} />;
};
export default OAuth2Client;