Skip to content

Commit 559f8f6

Browse files
authored
feat(sdk): add service user project management screens (#858)
1 parent 6d04de0 commit 559f8f6

File tree

7 files changed

+498
-24
lines changed

7 files changed

+498
-24
lines changed

sdks/js/packages/core/api-client/V1Beta1.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ import {
113113
V1Beta1EnableProjectResponse,
114114
V1Beta1EnableUserResponse,
115115
V1Beta1FeatureRequestBody,
116+
V1Beta1GenerateInvoicesRequest,
117+
V1Beta1GenerateInvoicesResponse,
116118
V1Beta1GetBillingAccountResponse,
117119
V1Beta1GetBillingBalanceResponse,
118120
V1Beta1GetCheckoutResponse,
@@ -193,6 +195,7 @@ import {
193195
V1Beta1ListRolesResponse,
194196
V1Beta1ListServiceUserCredentialsResponse,
195197
V1Beta1ListServiceUserJWKsResponse,
198+
V1Beta1ListServiceUserProjectsResponse,
196199
V1Beta1ListServiceUserTokensResponse,
197200
V1Beta1ListServiceUsersResponse,
198201
V1Beta1ListSubscriptionsResponse,
@@ -302,6 +305,25 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
302305
format: 'json',
303306
...params
304307
});
308+
/**
309+
* @description Triggers the creation of credit overdraft invoices for all billing accounts.
310+
*
311+
* @tags Invoice
312+
* @name AdminServiceGenerateInvoices
313+
* @summary Trigger invoice generation
314+
* @request POST:/v1beta1/admin/billing/invoices/generate
315+
* @secure
316+
*/
317+
adminServiceGenerateInvoices = (body: V1Beta1GenerateInvoicesRequest, params: RequestParams = {}) =>
318+
this.request<V1Beta1GenerateInvoicesResponse, RpcStatus>({
319+
path: `/v1beta1/admin/billing/invoices/generate`,
320+
method: 'POST',
321+
body: body,
322+
secure: true,
323+
type: ContentType.Json,
324+
format: 'json',
325+
...params
326+
});
305327
/**
306328
* @description Returns true if a principal has required permissions to access a resource and false otherwise.<br/> Note the principal can be a user, group or a service account.
307329
*
@@ -1482,6 +1504,16 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
14821504
user_id?: string;
14831505
/** The state to filter by. It can be `enabled` or `disabled`. */
14841506
state?: string;
1507+
/**
1508+
* The maximum number of users to return per page. The default is 1000.
1509+
* @format int32
1510+
*/
1511+
page_size?: number;
1512+
/**
1513+
* The page number to return. The default is 1.
1514+
* @format int32
1515+
*/
1516+
page_num?: number;
14851517
},
14861518
params: RequestParams = {}
14871519
) =>
@@ -1714,6 +1746,7 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
17141746
query?: {
17151747
permission_filter?: string;
17161748
with_roles?: boolean;
1749+
role_filters?: string[];
17171750
},
17181751
params: RequestParams = {}
17191752
) =>
@@ -3397,6 +3430,36 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
33973430
format: 'json',
33983431
...params
33993432
});
3433+
/**
3434+
* @description List all projects the service user belongs to
3435+
*
3436+
* @tags ServiceUser
3437+
* @name FrontierServiceListServiceUserProjects
3438+
* @summary List service sser projects
3439+
* @request GET:/v1beta1/organizations/{org_id}/serviceusers/{id}/projects
3440+
* @secure
3441+
*/
3442+
frontierServiceListServiceUserProjects = (
3443+
orgId: string,
3444+
id: string,
3445+
query?: {
3446+
/**
3447+
* list of permissions needs to be checked against each project
3448+
* query params are set as with_permissions=get&with_permissions=delete
3449+
* to be represented as array
3450+
*/
3451+
with_permissions?: string[];
3452+
},
3453+
params: RequestParams = {}
3454+
) =>
3455+
this.request<V1Beta1ListServiceUserProjectsResponse, RpcStatus>({
3456+
path: `/v1beta1/organizations/${orgId}/serviceusers/${id}/projects`,
3457+
method: 'GET',
3458+
query: query,
3459+
secure: true,
3460+
format: 'json',
3461+
...params
3462+
});
34003463
/**
34013464
* @description List all the credentials of a service user.
34023465
*
@@ -4357,7 +4420,7 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
43574420
frontierServiceListUsers = (
43584421
query?: {
43594422
/**
4360-
* The maximum number of users to return per page. The default is 50.
4423+
* The maximum number of users to return per page. The default is 1000.
43614424
* @format int32
43624425
*/
43634426
page_size?: number;
@@ -4782,6 +4845,16 @@ export class V1Beta1<SecurityDataType = unknown> extends HttpClient<SecurityData
47824845
*/
47834846
non_inherited?: boolean;
47844847
with_member_count?: boolean;
4848+
/**
4849+
* The maximum number of users to return per page. The default is 1000.
4850+
* @format int32
4851+
*/
4852+
page_size?: number;
4853+
/**
4854+
* The page number to return. The default is 1.
4855+
* @format int32
4856+
*/
4857+
page_num?: number;
47854858
},
47864859
params: RequestParams = {}
47874860
) =>

sdks/js/packages/core/api-client/data-contracts.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export interface ProtobufAny {
8080
* `NullValue` is a singleton enumeration to represent the null value for the
8181
* `Value` type union.
8282
*
83-
* The JSON representation for `NullValue` is JSON `null`.
83+
* The JSON representation for `NullValue` is JSON `null`.
8484
*
8585
* - NULL_VALUE: Null value.
8686
* @default "NULL_VALUE"
@@ -675,6 +675,10 @@ export interface V1Beta1FeatureRequestBody {
675675
metadata?: object;
676676
}
677677

678+
export type V1Beta1GenerateInvoicesRequest = object;
679+
680+
export type V1Beta1GenerateInvoicesResponse = object;
681+
678682
export interface V1Beta1GetBillingAccountResponse {
679683
/** Billing account */
680684
billing_account?: V1Beta1BillingAccount;
@@ -1170,6 +1174,8 @@ export interface V1Beta1ListProjectUsersResponseRolePair {
11701174
export interface V1Beta1ListProjectsByCurrentUserResponse {
11711175
projects?: V1Beta1Project[];
11721176
access_pairs?: V1Beta1ListProjectsByCurrentUserResponseAccessPair[];
1177+
/** @format int32 */
1178+
count?: number;
11731179
}
11741180

11751181
export interface V1Beta1ListProjectsByCurrentUserResponseAccessPair {
@@ -1206,6 +1212,16 @@ export interface V1Beta1ListServiceUserJWKsResponse {
12061212
keys?: V1Beta1ServiceUserJWK[];
12071213
}
12081214

1215+
export interface V1Beta1ListServiceUserProjectsResponse {
1216+
projects?: V1Beta1Project[];
1217+
access_pairs?: V1Beta1ListServiceUserProjectsResponseAccessPair[];
1218+
}
1219+
1220+
export interface V1Beta1ListServiceUserProjectsResponseAccessPair {
1221+
project_id?: string;
1222+
permissions?: string[];
1223+
}
1224+
12091225
export interface V1Beta1ListServiceUserTokensResponse {
12101226
tokens?: V1Beta1ServiceUserToken[];
12111227
}

sdks/js/packages/core/react/components/organization/api-keys/add.tsx

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import {
33
Separator,
44
Image,
55
InputField,
6-
TextField
6+
TextField,
7+
Select
78
} from '@raystack/apsara';
89
import styles from './styles.module.css';
910
import { Button, Flex, Text, toast } from '@raystack/apsara/v1';
@@ -13,13 +14,17 @@ import { Controller, useForm } from 'react-hook-form';
1314
import { useFrontier } from '~/react/contexts/FrontierContext';
1415
import * as yup from 'yup';
1516
import { yupResolver } from '@hookform/resolvers/yup';
16-
import { useCallback } from 'react';
17+
import { useCallback, useEffect, useState } from 'react';
18+
import { V1Beta1CreatePolicyForProjectBody, V1Beta1Project } from '~/src';
19+
import Skeleton from 'react-loading-skeleton';
20+
import { PERMISSIONS } from '~/utils';
1721

1822
const DEFAULT_KEY_NAME = 'Initial Generated Key';
1923

2024
const serviceAccountSchema = yup
2125
.object({
22-
title: yup.string().required('Name is a required field')
26+
title: yup.string().required('Name is a required field'),
27+
project_id: yup.string().required('Project is a required field')
2328
})
2429
.required();
2530

@@ -29,6 +34,10 @@ export const AddServiceAccount = () => {
2934
const navigate = useNavigate({ from: '/api-keys/add' });
3035
const { client, activeOrganization: organization } = useFrontier();
3136

37+
const [projects, setProjects] = useState<V1Beta1Project[]>([]);
38+
39+
const [isProjectsLoading, setIsProjectsLoading] = useState(false);
40+
3241
const {
3342
control,
3443
handleSubmit,
@@ -37,11 +46,11 @@ export const AddServiceAccount = () => {
3746
resolver: yupResolver(serviceAccountSchema)
3847
});
3948

40-
const orgId = organization?.id;
49+
const orgId = organization?.id || '';
4150

4251
const onSubmit = useCallback(
4352
async (data: FormData) => {
44-
if (!client) return;
53+
if (!client || !orgId) return;
4554
if (!orgId) return;
4655

4756
try {
@@ -52,6 +61,17 @@ export const AddServiceAccount = () => {
5261
});
5362

5463
if (serviceuser?.id) {
64+
const principal = `${PERMISSIONS.ServiceUserPrincipal}:${serviceuser?.id}`;
65+
66+
const policy: V1Beta1CreatePolicyForProjectBody = {
67+
role_id: PERMISSIONS.RoleProjectViewer,
68+
principal
69+
};
70+
await client?.frontierServiceCreatePolicyForProject(
71+
data?.project_id,
72+
policy
73+
);
74+
5575
const {
5676
data: { token }
5777
} = await client.frontierServiceCreateServiceUserToken(
@@ -79,8 +99,32 @@ export const AddServiceAccount = () => {
7999
[client, navigate, orgId]
80100
);
81101

102+
useEffect(() => {
103+
async function fetchProjects() {
104+
try {
105+
setIsProjectsLoading(true);
106+
const data = await client?.frontierServiceListOrganizationProjects(
107+
orgId
108+
);
109+
const list = data?.data?.projects?.sort((a, b) =>
110+
(a?.title || '') > (b?.title || '') ? 1 : -1
111+
);
112+
setProjects(list || []);
113+
} catch (error: unknown) {
114+
console.error(error);
115+
} finally {
116+
setIsProjectsLoading(false);
117+
}
118+
}
119+
if (orgId) {
120+
fetchProjects();
121+
}
122+
}, [client, orgId]);
123+
82124
const isDisabled = isSubmitting;
83125

126+
const isLoading = isProjectsLoading;
127+
84128
return (
85129
<Dialog open={true}>
86130
{/* @ts-ignore */}
@@ -116,21 +160,62 @@ export const AddServiceAccount = () => {
116160
</Text>
117161

118162
<InputField label="Name">
119-
<Controller
120-
render={({ field }) => (
121-
<TextField
122-
{...field}
123-
size="medium"
124-
placeholder="Provide service account name"
125-
/>
126-
)}
127-
name="title"
128-
control={control}
129-
/>
163+
{isLoading ? (
164+
<Skeleton height={'25px'} />
165+
) : (
166+
<Controller
167+
render={({ field }) => (
168+
<TextField
169+
{...field}
170+
size="medium"
171+
placeholder="Provide service account name"
172+
/>
173+
)}
174+
name="title"
175+
control={control}
176+
/>
177+
)}
130178
<Text size={1} variant="danger">
131179
{errors.title && String(errors.title?.message)}
132180
</Text>
133181
</InputField>
182+
<InputField label="Project">
183+
{isLoading ? (
184+
<Skeleton height={'25px'} />
185+
) : (
186+
<Controller
187+
render={({ field }) => {
188+
const { ref, onChange, ...rest } = field;
189+
return (
190+
<Select {...rest} onValueChange={onChange}>
191+
<Select.Trigger ref={ref}>
192+
<Select.Value placeholder="Select a project" />
193+
</Select.Trigger>
194+
<Select.Content
195+
style={{ width: '100% !important', zIndex: 65 }}
196+
>
197+
<Select.Viewport style={{ maxHeight: '300px' }}>
198+
{projects.map(project => (
199+
<Select.Item
200+
value={project.id || ''}
201+
key={project.id}
202+
>
203+
{project.title}
204+
</Select.Item>
205+
))}
206+
</Select.Viewport>
207+
</Select.Content>
208+
</Select>
209+
);
210+
}}
211+
name="project_id"
212+
control={control}
213+
/>
214+
)}
215+
<Text size={1} variant="danger">
216+
{errors.project_id && String(errors.project_id?.message)}
217+
</Text>
218+
</InputField>
134219
</Flex>
135220
<Separator />
136221
<Flex justify="end" className={styles.addDialogFormBtnWrapper}>
@@ -139,9 +224,9 @@ export const AddServiceAccount = () => {
139224
size="normal"
140225
type="submit"
141226
data-test-id="frontier-sdk-add-service-account-btn"
142-
loading={isSubmitting}
143-
disabled={isDisabled}
144-
loaderText={'Creating...'}
227+
loading={isSubmitting || isLoading}
228+
disabled={isDisabled || isLoading}
229+
loaderText={isLoading ? 'Loading...' : 'Creating...'}
145230
>
146231
Create
147232
</Button>

0 commit comments

Comments
 (0)