Skip to content

Commit 8df03bf

Browse files
authored
Support for experimental plan management tab in UI (#4549)
2 parents f4d61f2 + 55f559e commit 8df03bf

File tree

14 files changed

+228
-5
lines changed

14 files changed

+228
-5
lines changed

crates/cli/src/util.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ pub fn site_config_from_config(
223223
minimum_password_complexity: password_config.minimum_complexity(),
224224
session_expiration,
225225
login_with_email_allowed: account_config.login_with_email_allowed,
226+
plan_management_iframe_uri: experimental_config.plan_management_iframe_uri.clone(),
226227
})
227228
}
228229

crates/config/src/sections/experimental.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ pub struct ExperimentalConfig {
7575
/// Disabled by default
7676
#[serde(skip_serializing_if = "Option::is_none")]
7777
pub inactive_session_expiration: Option<InactiveSessionExpirationConfig>,
78+
79+
/// Experimental feature to show a plan management tab and iframe.
80+
/// This value is passed through "as is" to the client without any
81+
/// validation.
82+
#[serde(skip_serializing_if = "Option::is_none")]
83+
pub plan_management_iframe_uri: Option<String>,
7884
}
7985

8086
impl Default for ExperimentalConfig {
@@ -83,6 +89,7 @@ impl Default for ExperimentalConfig {
8389
access_token_ttl: default_token_ttl(),
8490
compat_token_ttl: default_token_ttl(),
8591
inactive_session_expiration: None,
92+
plan_management_iframe_uri: None,
8693
}
8794
}
8895
}
@@ -92,6 +99,7 @@ impl ExperimentalConfig {
9299
is_default_token_ttl(&self.access_token_ttl)
93100
&& is_default_token_ttl(&self.compat_token_ttl)
94101
&& self.inactive_session_expiration.is_none()
102+
&& self.plan_management_iframe_uri.is_none()
95103
}
96104
}
97105

crates/data-model/src/site_config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,7 @@ pub struct SiteConfig {
9393

9494
/// Whether users can log in with their email address.
9595
pub login_with_email_allowed: bool,
96+
97+
/// The iframe URL to show in the plan tab of the UI
98+
pub plan_management_iframe_uri: Option<String>,
9699
}

crates/handlers/src/graphql/model/site_config.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ pub struct SiteConfig {
5656

5757
/// Whether users can log in with their email address.
5858
login_with_email_allowed: bool,
59+
60+
/// Experimental plan management iframe URI.
61+
plan_management_iframe_uri: Option<String>,
5962
}
6063

6164
#[derive(SimpleObject)]
@@ -102,6 +105,7 @@ impl SiteConfig {
102105
account_deactivation_allowed: data_model.account_deactivation_allowed,
103106
minimum_password_complexity: data_model.minimum_password_complexity,
104107
login_with_email_allowed: data_model.login_with_email_allowed,
108+
plan_management_iframe_uri: data_model.plan_management_iframe_uri.clone(),
105109
}
106110
}
107111
}

crates/handlers/src/test_utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub fn test_site_config() -> SiteConfig {
146146
minimum_password_complexity: 1,
147147
session_expiration: None,
148148
login_with_email_allowed: true,
149+
plan_management_iframe_uri: None,
149150
}
150151
}
151152

docs/config.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2567,6 +2567,10 @@
25672567
"$ref": "#/definitions/InactiveSessionExpirationConfig"
25682568
}
25692569
]
2570+
},
2571+
"plan_management_iframe_uri": {
2572+
"description": "Experimental feature to show a plan management tab and iframe. This value is passed through \"as is\" to the client without any validation.",
2573+
"type": "string"
25702574
}
25712575
}
25722576
},

frontend/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
},
123123
"nav": {
124124
"devices": "Devices",
125+
"plan": "Plan",
125126
"settings": "Settings"
126127
},
127128
"not_found_alert_title": "Not found.",

frontend/schema.graphql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1758,6 +1758,10 @@ type SiteConfig implements Node {
17581758
"""
17591759
loginWithEmailAllowed: Boolean!
17601760
"""
1761+
Experimental plan management iframe URI.
1762+
"""
1763+
planManagementIframeUri: String
1764+
"""
17611765
The ID of the site configuration.
17621766
"""
17631767
id: ID!

frontend/src/gql/gql.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ type Documents = {
5050
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": typeof types.UserEmailList_SiteConfigFragmentDoc,
5151
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": typeof types.BrowserSessionsOverview_UserFragmentDoc,
5252
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": typeof types.UserProfileDocument,
53+
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": typeof types.PlanManagementTabDocument,
5354
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": typeof types.BrowserSessionListDocument,
5455
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": typeof types.SessionsOverviewDocument,
5556
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": typeof types.AppSessionsListDocument,
56-
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": typeof types.CurrentUserGreetingDocument,
57+
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": typeof types.CurrentUserGreetingDocument,
5758
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": typeof types.OAuth2ClientDocument,
5859
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.CurrentViewerDocument,
5960
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": typeof types.DeviceRedirectDocument,
@@ -106,10 +107,11 @@ const documents: Documents = {
106107
"\n fragment UserEmailList_siteConfig on SiteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n }\n": types.UserEmailList_SiteConfigFragmentDoc,
107108
"\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc,
108109
"\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n": types.UserProfileDocument,
110+
"\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n": types.PlanManagementTabDocument,
109111
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n $lastActive: DateFilter\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n lastActive: $lastActive\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
110112
"\n query SessionsOverview {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewDocument,
111113
"\n query AppSessionsList(\n $before: String\n $after: String\n $first: Int\n $last: Int\n $lastActive: DateFilter\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n lastActive: $lastActive\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListDocument,
112-
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n": types.CurrentUserGreetingDocument,
114+
"\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n": types.CurrentUserGreetingDocument,
113115
"\n query OAuth2Client($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientDocument,
114116
"\n query CurrentViewer {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerDocument,
115117
"\n query DeviceRedirect($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectDocument,
@@ -267,6 +269,10 @@ export function graphql(source: "\n fragment BrowserSessionsOverview_user on Us
267269
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
268270
*/
269271
export function graphql(source: "\n query UserProfile {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n user {\n ...AddEmailForm_user\n ...UserEmailList_user\n ...AccountDeleteButton_user\n hasPassword\n emails(first: 0) {\n totalCount\n }\n }\n }\n }\n\n siteConfig {\n emailChangeAllowed\n passwordLoginEnabled\n accountDeactivationAllowed\n ...AddEmailForm_siteConfig\n ...UserEmailList_siteConfig\n ...PasswordChange_siteConfig\n ...AccountDeleteButton_siteConfig\n }\n }\n"): typeof import('./graphql').UserProfileDocument;
272+
/**
273+
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
274+
*/
275+
export function graphql(source: "\n query PlanManagementTab {\n siteConfig {\n planManagementIframeUri\n }\n }\n"): typeof import('./graphql').PlanManagementTabDocument;
270276
/**
271277
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
272278
*/
@@ -282,7 +288,7 @@ export function graphql(source: "\n query AppSessionsList(\n $before: String
282288
/**
283289
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
284290
*/
285-
export function graphql(source: "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
291+
export function graphql(source: "\n query CurrentUserGreeting {\n viewer {\n __typename\n ... on User {\n ...UserGreeting_user\n }\n }\n\n siteConfig {\n ...UserGreeting_siteConfig\n planManagementIframeUri\n }\n }\n"): typeof import('./graphql').CurrentUserGreetingDocument;
286292
/**
287293
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
288294
*/

frontend/src/gql/graphql.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,8 @@ export type SiteConfig = Node & {
12961296
passwordLoginEnabled: Scalars['Boolean']['output'];
12971297
/** Whether passwords are enabled and users can register using a password. */
12981298
passwordRegistrationEnabled: Scalars['Boolean']['output'];
1299+
/** Experimental plan management iframe URI. */
1300+
planManagementIframeUri?: Maybe<Scalars['String']['output']>;
12991301
/** The URL to the privacy policy. */
13001302
policyUri?: Maybe<Scalars['Url']['output']>;
13011303
/** The server name of the homeserver. */
@@ -1858,6 +1860,11 @@ export type UserProfileQuery = { __typename?: 'Query', viewerSession: { __typena
18581860
& { ' $fragmentRefs'?: { 'AddEmailForm_SiteConfigFragment': AddEmailForm_SiteConfigFragment;'UserEmailList_SiteConfigFragment': UserEmailList_SiteConfigFragment;'PasswordChange_SiteConfigFragment': PasswordChange_SiteConfigFragment;'AccountDeleteButton_SiteConfigFragment': AccountDeleteButton_SiteConfigFragment } }
18591861
) };
18601862

1863+
export type PlanManagementTabQueryVariables = Exact<{ [key: string]: never; }>;
1864+
1865+
1866+
export type PlanManagementTabQuery = { __typename?: 'Query', siteConfig: { __typename?: 'SiteConfig', planManagementIframeUri?: string | null } };
1867+
18611868
export type BrowserSessionListQueryVariables = Exact<{
18621869
first?: InputMaybe<Scalars['Int']['input']>;
18631870
after?: InputMaybe<Scalars['String']['input']>;
@@ -1904,7 +1911,7 @@ export type CurrentUserGreetingQuery = { __typename?: 'Query', viewer: { __typen
19041911
{ __typename: 'User' }
19051912
& { ' $fragmentRefs'?: { 'UserGreeting_UserFragment': UserGreeting_UserFragment } }
19061913
), siteConfig: (
1907-
{ __typename?: 'SiteConfig' }
1914+
{ __typename?: 'SiteConfig', planManagementIframeUri?: string | null }
19081915
& { ' $fragmentRefs'?: { 'UserGreeting_SiteConfigFragment': UserGreeting_SiteConfigFragment } }
19091916
) };
19101917

@@ -2576,6 +2583,13 @@ fragment UserEmailList_siteConfig on SiteConfig {
25762583
emailChangeAllowed
25772584
passwordLoginEnabled
25782585
}`) as unknown as TypedDocumentString<UserProfileQuery, UserProfileQueryVariables>;
2586+
export const PlanManagementTabDocument = new TypedDocumentString(`
2587+
query PlanManagementTab {
2588+
siteConfig {
2589+
planManagementIframeUri
2590+
}
2591+
}
2592+
`) as unknown as TypedDocumentString<PlanManagementTabQuery, PlanManagementTabQueryVariables>;
25792593
export const BrowserSessionListDocument = new TypedDocumentString(`
25802594
query BrowserSessionList($first: Int, $after: String, $last: Int, $before: String, $lastActive: DateFilter) {
25812595
viewerSession {
@@ -2763,6 +2777,7 @@ export const CurrentUserGreetingDocument = new TypedDocumentString(`
27632777
}
27642778
siteConfig {
27652779
...UserGreeting_siteConfig
2780+
planManagementIframeUri
27662781
}
27672782
}
27682783
fragment UserGreeting_user on User {
@@ -3281,6 +3296,27 @@ export const mockUserProfileQuery = (resolver: GraphQLResponseResolver<UserProfi
32813296
options
32823297
)
32833298

3299+
/**
3300+
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
3301+
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))
3302+
* @see https://mswjs.io/docs/basics/response-resolver
3303+
* @example
3304+
* mockPlanManagementTabQuery(
3305+
* ({ query, variables }) => {
3306+
* return HttpResponse.json({
3307+
* data: { siteConfig }
3308+
* })
3309+
* },
3310+
* requestOptions
3311+
* )
3312+
*/
3313+
export const mockPlanManagementTabQuery = (resolver: GraphQLResponseResolver<PlanManagementTabQuery, PlanManagementTabQueryVariables>, options?: RequestHandlerOptions) =>
3314+
graphql.query<PlanManagementTabQuery, PlanManagementTabQueryVariables>(
3315+
'PlanManagementTab',
3316+
resolver,
3317+
options
3318+
)
3319+
32843320
/**
32853321
* @param resolver A function that accepts [resolver arguments](https://mswjs.io/docs/api/graphql#resolver-argument) and must always return the instruction on what to do with the intercepted request. ([see more](https://mswjs.io/docs/concepts/response-resolver#resolver-instructions))
32863322
* @param options Options object to customize the behavior of the mock. ([see more](https://mswjs.io/docs/api/graphql#handler-options))

0 commit comments

Comments
 (0)