Skip to content

Commit 418b5ed

Browse files
committed
feat(front): use rbac to load service accounts
1 parent d3486a4 commit 418b5ed

30 files changed

+615
-314
lines changed

front/assets/js/service_accounts/components/CreateServiceAccount.tsx

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServi
1717

1818
const [name, setName] = useState(``);
1919
const [description, setDescription] = useState(``);
20+
const [selectedRoleId, setSelectedRoleId] = useState(``);
2021
const [loading, setLoading] = useState(false);
21-
const [error, setError] = useState<string | null>(null);
22-
const [token, setToken] = useState<string | null>(null);
22+
const [error, setError] = useState(``);
23+
const [token, setToken] = useState(``);
2324

2425
const handleSubmit = async (e: Event) => {
2526
e.preventDefault();
2627
setError(null);
2728
setLoading(true);
2829

29-
const response = await api.create(name, description);
30+
const response = await api.create(name, description, selectedRoleId);
3031

3132
if (response.error) {
3233
setError(response.error);
@@ -43,12 +44,13 @@ export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServi
4344
}
4445
setName(``);
4546
setDescription(``);
46-
setError(null);
47-
setToken(null);
47+
setSelectedRoleId(``);
48+
setError(``);
49+
setToken(``);
4850
onClose();
4951
};
5052

51-
const canSubmit = name.trim().length > 0 && !loading;
53+
const canSubmit = name.trim().length > 0 && selectedRoleId.length > 0 && !loading;
5254

5355
return (
5456
<Modal isOpen={isOpen} close={handleClose} title="Create Service Account">
@@ -80,6 +82,23 @@ export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServi
8082
/>
8183
</div>
8284

85+
<div className="mb3">
86+
<label className="db mb2 f6 b">Role *</label>
87+
<select
88+
className="form-control w-100"
89+
value={selectedRoleId}
90+
onChange={(e) => setSelectedRoleId(e.currentTarget.value)}
91+
disabled={loading}
92+
>
93+
<option value="">Select a role...</option>
94+
{config.roles.map((role) => (
95+
<option key={role.id} value={role.id}>
96+
{role.name} - {role.description}
97+
</option>
98+
))}
99+
</select>
100+
</div>
101+
83102
{error && (
84103
<div className="bg-washed-red ba b--red br2 pa2 mb3">
85104
<p className="f6 mb0 red">{error}</p>

front/assets/js/service_accounts/components/EditServiceAccount.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ export const EditServiceAccount = ({
2323

2424
const [name, setName] = useState(``);
2525
const [description, setDescription] = useState(``);
26+
const [selectedRoleId, setSelectedRoleId] = useState(``);
2627
const [loading, setLoading] = useState(false);
27-
const [error, setError] = useState<string | null>(null);
28+
const [error, setError] = useState(``);
2829

2930
useEffect(() => {
3031
if (serviceAccount) {
3132
setName(serviceAccount.name);
3233
setDescription(serviceAccount.description);
34+
setSelectedRoleId(serviceAccount.roles.find((role) => role.source == `manual`)?.id || ``);
3335
}
3436
}, [serviceAccount]);
3537

@@ -40,7 +42,7 @@ export const EditServiceAccount = ({
4042
setError(null);
4143
setLoading(true);
4244

43-
const response = await api.update(serviceAccount.id, name, description);
45+
const response = await api.update(serviceAccount.id, name, description, selectedRoleId);
4446

4547
if (response.error) {
4648
setError(response.error);
@@ -59,7 +61,8 @@ export const EditServiceAccount = ({
5961

6062
const hasChanges = serviceAccount && (
6163
name !== serviceAccount.name ||
62-
description !== serviceAccount.description
64+
description !== serviceAccount.description ||
65+
selectedRoleId !== serviceAccount.roles.find((role) => role.source == `manual`)?.id
6366
);
6467

6568
const canSubmit = name.trim().length > 0 && !loading && hasChanges;
@@ -69,7 +72,7 @@ export const EditServiceAccount = ({
6972
return (
7073
<Modal isOpen={isOpen} close={handleClose} title="Edit Service Account">
7174
<form onSubmit={(e) => void handleSubmit(e)}>
72-
<div className="pa4">
75+
<div className="pa3">
7376
<div className="mb3">
7477
<label className="db mb2 f6 b">Name *</label>
7578
<input
@@ -95,14 +98,32 @@ export const EditServiceAccount = ({
9598
/>
9699
</div>
97100

101+
102+
<div className="mb3">
103+
<label className="db mb2 f6 b">Role *</label>
104+
<select
105+
className="form-control w-100"
106+
value={selectedRoleId}
107+
onChange={(e) => setSelectedRoleId(e.currentTarget.value)}
108+
disabled={loading}
109+
>
110+
<option value="">Select a role...</option>
111+
{config.roles.map((role) => (
112+
<option key={role.id} value={role.id}>
113+
{role.name} - {role.description}
114+
</option>
115+
))}
116+
</select>
117+
</div>
118+
98119
{error && (
99120
<div className="bg-washed-red ba b--red br2 pa2 mb3">
100121
<p className="f6 mb0 red">{error}</p>
101122
</div>
102123
)}
103124
</div>
104125

105-
<div className="flex justify-end items-center pa4 bt b--black-10">
126+
<div className="flex justify-end items-center pa3 bt b--black-10">
106127
<button
107128
type="button"
108129
className="btn btn-secondary mr3"

front/assets/js/service_accounts/components/ServiceAccountsList.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const ServiceAccountsList = ({
5151
className="btn btn-primary flex items-center"
5252
onClick={onCreateNew}
5353
>
54-
<span className="material-symbols-outlined mr2">add</span>
54+
<span className="material-symbols-outlined mr2">smart_toy</span>
5555
Create Service Account
5656
</button>
5757
)}
@@ -77,7 +77,7 @@ export const ServiceAccountsList = ({
7777
className="btn btn-primary flex items-center"
7878
onClick={onCreateNew}
7979
>
80-
<span className="material-symbols-outlined mr2">add</span>
80+
<span className="material-symbols-outlined mr2">smart_toy</span>
8181
Create Service Account
8282
</button>
8383
)}
@@ -97,15 +97,22 @@ export const ServiceAccountsList = ({
9797
<div key={account.id} className={`bg-white shadow-1 ph3 pv2 ${idx == serviceAccounts.length - 1 ? `br2 br--bottom` : ``}`}>
9898
<div className="flex items-center justify-between" style={{ minHeight: `45px` }}>
9999
<div className="flex items-center">
100-
<span className="material-symbols-outlined mr2 f4 gray">key</span>
100+
<div className={`br2 ${account.deactivated ? `bg-washed-red red` : `bg-washed-green green`} br-100 w2 h2 flex items-center justify-center mr2`}>
101+
<toolbox.Tooltip
102+
anchor={<span className={`material-symbols-outlined`}>smart_toy</span>}
103+
content={account.deactivated ? `Deactivated account` : `Active account`}
104+
/>
105+
106+
</div>
101107
<div>
102108
<div className="flex items-center">
103109
<span className="b black">{account.name}</span>
104-
{account.deactivated ? (
105-
<span className="ml2 f7 red bg-washed-red ph2 pv1 br2">Deactivated</span>
106-
) : (
107-
<span className="ml2 f7 green bg-washed-green ph2 pv1 br2">Active</span>
108-
)}
110+
{(() => {
111+
const role = account.roles.find((role) => role.source == `manual`);
112+
if(role){
113+
return <span className={`f6 normal ml1 ph1 br2 bg-${role.color} white`}>{role.name}</span>;
114+
}
115+
})()}
109116
</div>
110117
<div className="f7 gray mt1">
111118
{account.description || `No description`} • Created {formatDate(account.created_at)}
@@ -149,7 +156,7 @@ export const ServiceAccountsList = ({
149156
)}
150157

151158
{hasMore && (
152-
<div className="tc pa4">
159+
<div className="tc pa3">
153160
<button
154161
className="btn btn-secondary"
155162
onClick={onLoadMore}

front/assets/js/service_accounts/components/TokenDisplay.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useState } from "preact/hooks";
21
import { Box } from "js/toolbox";
32

43
interface TokenDisplayProps {
@@ -7,20 +6,8 @@ interface TokenDisplayProps {
76
}
87

98
export const TokenDisplay = ({ token, onClose }: TokenDisplayProps) => {
10-
const [copied, setCopied] = useState(false);
11-
12-
const copyToClipboard = async () => {
13-
try {
14-
await navigator.clipboard.writeText(token);
15-
setCopied(true);
16-
setTimeout(() => setCopied(false), 2000);
17-
} catch (err) {
18-
// Failed to copy token
19-
}
20-
};
21-
229
return (
23-
<div className="pa4">
10+
<div className="pa3">
2411
<div className="mb3">
2512
<h3 className="f4 mb2">API Token Generated</h3>
2613
<p className="f6 gray mb3">

front/assets/js/service_accounts/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class AppConfig {
1515
canView: json.permissions?.view || false,
1616
canManage: json.permissions?.manage || false,
1717
},
18+
roles: json.roles || [],
1819
urls: {
1920
list: `/service_accounts`,
2021
create: `/service_accounts`,

front/assets/js/service_accounts/index.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Fragment, render } from "preact";
22
import { useState, useContext, useEffect, useCallback } from "preact/hooks";
33
import { Modal, Box } from "js/toolbox";
44
import { AppConfig, ConfigContext } from "./config";
5-
import { ServiceAccount, AppState, ModalState } from "./types";
5+
import { ServiceAccount, AppState } from "./types";
66
import { ServiceAccountsAPI } from "./utils/api";
77
import { ServiceAccountsList } from "./components/ServiceAccountsList";
88
import { CreateServiceAccount } from "./components/CreateServiceAccount";
@@ -33,20 +33,20 @@ const App = () => {
3333
loading: true,
3434
error: null,
3535
selectedServiceAccount: null,
36-
modalState: ModalState.Closed,
3736
newToken: null,
38-
nextPageToken: null,
37+
page: 1,
38+
totalPages: 0,
3939
});
4040

4141
const [createModalOpen, setCreateModalOpen] = useState(false);
4242
const [editModalOpen, setEditModalOpen] = useState(false);
4343
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
4444
const [regenerateModalOpen, setRegenerateModalOpen] = useState(false);
4545

46-
const loadServiceAccounts = useCallback(async (pageToken?: string) => {
46+
const loadServiceAccounts = useCallback(async (page?: number) => {
4747
setState(prev => ({ ...prev, loading: true, error: null }));
4848

49-
const response = await api.list(pageToken);
49+
const response = await api.list(page);
5050

5151
if (response.error) {
5252
setState(prev => ({
@@ -58,10 +58,11 @@ const App = () => {
5858
setState(prev => ({
5959
...prev,
6060
loading: false,
61-
serviceAccounts: pageToken
61+
page: page || 1,
62+
serviceAccounts: page > 1
6263
? [...prev.serviceAccounts, ...response.data.items]
6364
: response.data.items,
64-
nextPageToken: response.data.next_page_token || null,
65+
totalPages: response.data.totalPages || null,
6566
}));
6667
}
6768
}, []);
@@ -119,9 +120,11 @@ const App = () => {
119120
void loadServiceAccounts();
120121
};
121122

123+
const hasMorePages = state.totalPages && state.page < state.totalPages;
124+
122125
const handleLoadMore = () => {
123-
if (state.nextPageToken) {
124-
void loadServiceAccounts(state.nextPageToken);
126+
if (hasMorePages) {
127+
void loadServiceAccounts(state.page + 1);
125128
}
126129
};
127130

@@ -140,7 +143,7 @@ const App = () => {
140143
onDelete={handleDelete}
141144
onRegenerateToken={handleRegenerateToken}
142145
onLoadMore={handleLoadMore}
143-
hasMore={!!state.nextPageToken}
146+
hasMore={hasMorePages}
144147
onCreateNew={() => setCreateModalOpen(true)}
145148
/>
146149

@@ -165,7 +168,7 @@ const App = () => {
165168
close={() => setDeleteModalOpen(false)}
166169
title="Delete Service Account"
167170
>
168-
<div className="pa4">
171+
<div className="pa3">
169172
<p className="f5 mb3">
170173
Are you sure you want to delete the service account{` `}
171174
<strong>{state.selectedServiceAccount?.name}</strong>?
@@ -174,7 +177,7 @@ const App = () => {
174177
This will immediately revoke API access. This action cannot be undone.
175178
</p>
176179
</div>
177-
<div className="flex justify-end items-center pa4 bt b--black-10">
180+
<div className="flex justify-end items-center pa3 bt b--black-10">
178181
<button
179182
className="btn btn-secondary mr3"
180183
onClick={() => setDeleteModalOpen(false)}
@@ -196,7 +199,7 @@ const App = () => {
196199
close={() => setRegenerateModalOpen(false)}
197200
title="Regenerate API Token"
198201
>
199-
<div className="pa4">
202+
<div className="pa3">
200203
<p className="f5 mb3">
201204
Are you sure you want to regenerate the API token for{` `}
202205
<strong>{state.selectedServiceAccount?.name}</strong>?
@@ -207,7 +210,7 @@ const App = () => {
207210
Any systems using the old token will lose access.
208211
</Box>
209212
</div>
210-
<div className="flex justify-end items-center pa4 bt b--black-10">
213+
<div className="flex justify-end items-center pa3 bt b--black-10">
211214
<button
212215
className="btn btn-secondary mr3"
213216
onClick={() => setRegenerateModalOpen(false)}

0 commit comments

Comments
 (0)