Skip to content

feat(front): add service accounts feature #435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServi

const [name, setName] = useState(``);
const [description, setDescription] = useState(``);
const [selectedRoleId, setSelectedRoleId] = useState(``);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [token, setToken] = useState<string | null>(null);
const [error, setError] = useState(``);
const [token, setToken] = useState(``);

const handleSubmit = async (e: Event) => {
e.preventDefault();
setError(null);
setLoading(true);

const response = await api.create(name, description);
const response = await api.create(name, description, selectedRoleId);

if (response.error) {
setError(response.error);
Expand All @@ -43,12 +44,13 @@ export const CreateServiceAccount = ({ isOpen, onClose, onCreated }: CreateServi
}
setName(``);
setDescription(``);
setError(null);
setToken(null);
setSelectedRoleId(``);
setError(``);
setToken(``);
onClose();
};

const canSubmit = name.trim().length > 0 && !loading;
const canSubmit = name.trim().length > 0 && selectedRoleId.length > 0 && !loading;

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

<div className="mb3">
<label className="db mb2 f6 b">Role *</label>
<select
className="form-control w-100"
value={selectedRoleId}
onChange={(e) => setSelectedRoleId(e.currentTarget.value)}
disabled={loading}
>
<option value="">Select a role...</option>
{config.roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name} - {role.description}
</option>
))}
</select>
</div>

{error && (
<div className="bg-washed-red ba b--red br2 pa2 mb3">
<p className="f6 mb0 red">{error}</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ export const EditServiceAccount = ({

const [name, setName] = useState(``);
const [description, setDescription] = useState(``);
const [selectedRoleId, setSelectedRoleId] = useState(``);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [error, setError] = useState(``);

useEffect(() => {
if (serviceAccount) {
setName(serviceAccount.name);
setDescription(serviceAccount.description);
setSelectedRoleId(serviceAccount.roles.find((role) => role.source == `manual`)?.id || ``);
}
}, [serviceAccount]);

Expand All @@ -40,7 +42,7 @@ export const EditServiceAccount = ({
setError(null);
setLoading(true);

const response = await api.update(serviceAccount.id, name, description);
const response = await api.update(serviceAccount.id, name, description, selectedRoleId);

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

const hasChanges = serviceAccount && (
name !== serviceAccount.name ||
description !== serviceAccount.description
description !== serviceAccount.description ||
selectedRoleId !== serviceAccount.roles.find((role) => role.source == `manual`)?.id
);

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


<div className="mb3">
<label className="db mb2 f6 b">Role *</label>
<select
className="form-control w-100"
value={selectedRoleId}
onChange={(e) => setSelectedRoleId(e.currentTarget.value)}
disabled={loading}
>
<option value="">Select a role...</option>
{config.roles.map((role) => (
<option key={role.id} value={role.id}>
{role.name} - {role.description}
</option>
))}
</select>
</div>

{error && (
<div className="bg-washed-red ba b--red br2 pa2 mb3">
<p className="f6 mb0 red">{error}</p>
</div>
)}
</div>

<div className="flex justify-end items-center pa4 bt b--black-10">
<div className="flex justify-end items-center pa3 bt b--black-10">
<button
type="button"
className="btn btn-secondary mr3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const ServiceAccountsList = ({
className="btn btn-primary flex items-center"
onClick={onCreateNew}
>
<span className="material-symbols-outlined mr2">add</span>
<span className="material-symbols-outlined mr2">smart_toy</span>
Create Service Account
</button>
)}
Expand All @@ -77,7 +77,7 @@ export const ServiceAccountsList = ({
className="btn btn-primary flex items-center"
onClick={onCreateNew}
>
<span className="material-symbols-outlined mr2">add</span>
<span className="material-symbols-outlined mr2">smart_toy</span>
Create Service Account
</button>
)}
Expand All @@ -97,15 +97,22 @@ export const ServiceAccountsList = ({
<div key={account.id} className={`bg-white shadow-1 ph3 pv2 ${idx == serviceAccounts.length - 1 ? `br2 br--bottom` : ``}`}>
<div className="flex items-center justify-between" style={{ minHeight: `45px` }}>
<div className="flex items-center">
<span className="material-symbols-outlined mr2 f4 gray">key</span>
<div className={`br2 ${account.deactivated ? `bg-washed-red red` : `bg-washed-green green`} br-100 w2 h2 flex items-center justify-center mr2`}>
<toolbox.Tooltip
anchor={<span className={`material-symbols-outlined`}>smart_toy</span>}
content={account.deactivated ? `Deactivated account` : `Active account`}
/>

</div>
<div>
<div className="flex items-center">
<span className="b black">{account.name}</span>
{account.deactivated ? (
<span className="ml2 f7 red bg-washed-red ph2 pv1 br2">Deactivated</span>
) : (
<span className="ml2 f7 green bg-washed-green ph2 pv1 br2">Active</span>
)}
{(() => {
const role = account.roles.find((role) => role.source == `manual`);
if(role){
return <span className={`f6 normal ml1 ph1 br2 bg-${role.color} white`}>{role.name}</span>;
}
})()}
</div>
<div className="f7 gray mt1">
{account.description || `No description`} β€’ Created {formatDate(account.created_at)}
Expand Down Expand Up @@ -149,7 +156,7 @@ export const ServiceAccountsList = ({
)}

{hasMore && (
<div className="tc pa4">
<div className="tc pa3">
<button
className="btn btn-secondary"
onClick={onLoadMore}
Expand Down
15 changes: 1 addition & 14 deletions front/assets/js/service_accounts/components/TokenDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from "preact/hooks";
import { Box } from "js/toolbox";

interface TokenDisplayProps {
Expand All @@ -7,20 +6,8 @@ interface TokenDisplayProps {
}

export const TokenDisplay = ({ token, onClose }: TokenDisplayProps) => {
const [copied, setCopied] = useState(false);

const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(token);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
// Failed to copy token
}
};

return (
<div className="pa4">
<div className="pa3">
<div className="mb3">
<h3 className="f4 mb2">API Token Generated</h3>
<p className="f6 gray mb3">
Expand Down
1 change: 1 addition & 0 deletions front/assets/js/service_accounts/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class AppConfig {
canView: json.permissions?.view || false,
canManage: json.permissions?.manage || false,
},
roles: json.roles || [],
urls: {
list: `/service_accounts`,
create: `/service_accounts`,
Expand Down
31 changes: 17 additions & 14 deletions front/assets/js/service_accounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Fragment, render } from "preact";
import { useState, useContext, useEffect, useCallback } from "preact/hooks";
import { Modal, Box } from "js/toolbox";
import { AppConfig, ConfigContext } from "./config";
import { ServiceAccount, AppState, ModalState } from "./types";
import { ServiceAccount, AppState } from "./types";
import { ServiceAccountsAPI } from "./utils/api";
import { ServiceAccountsList } from "./components/ServiceAccountsList";
import { CreateServiceAccount } from "./components/CreateServiceAccount";
Expand Down Expand Up @@ -33,20 +33,20 @@ const App = () => {
loading: true,
error: null,
selectedServiceAccount: null,
modalState: ModalState.Closed,
newToken: null,
nextPageToken: null,
page: 1,
totalPages: 0,
});

const [createModalOpen, setCreateModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [regenerateModalOpen, setRegenerateModalOpen] = useState(false);

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

const response = await api.list(pageToken);
const response = await api.list(page);

if (response.error) {
setState(prev => ({
Expand All @@ -58,10 +58,11 @@ const App = () => {
setState(prev => ({
...prev,
loading: false,
serviceAccounts: pageToken
page: page || 1,
serviceAccounts: page > 1
? [...prev.serviceAccounts, ...response.data.items]
: response.data.items,
nextPageToken: response.data.next_page_token || null,
totalPages: response.data.totalPages || null,
}));
}
}, []);
Expand Down Expand Up @@ -119,9 +120,11 @@ const App = () => {
void loadServiceAccounts();
};

const hasMorePages = state.totalPages && state.page < state.totalPages;

const handleLoadMore = () => {
if (state.nextPageToken) {
void loadServiceAccounts(state.nextPageToken);
if (hasMorePages) {
void loadServiceAccounts(state.page + 1);
}
};

Expand All @@ -140,7 +143,7 @@ const App = () => {
onDelete={handleDelete}
onRegenerateToken={handleRegenerateToken}
onLoadMore={handleLoadMore}
hasMore={!!state.nextPageToken}
hasMore={hasMorePages}
onCreateNew={() => setCreateModalOpen(true)}
/>

Expand All @@ -165,7 +168,7 @@ const App = () => {
close={() => setDeleteModalOpen(false)}
title="Delete Service Account"
>
<div className="pa4">
<div className="pa3">
<p className="f5 mb3">
Are you sure you want to delete the service account{` `}
<strong>{state.selectedServiceAccount?.name}</strong>?
Expand All @@ -174,7 +177,7 @@ const App = () => {
This will immediately revoke API access. This action cannot be undone.
</p>
</div>
<div className="flex justify-end items-center pa4 bt b--black-10">
<div className="flex justify-end items-center pa3 bt b--black-10">
<button
className="btn btn-secondary mr3"
onClick={() => setDeleteModalOpen(false)}
Expand All @@ -196,7 +199,7 @@ const App = () => {
close={() => setRegenerateModalOpen(false)}
title="Regenerate API Token"
>
<div className="pa4">
<div className="pa3">
<p className="f5 mb3">
Are you sure you want to regenerate the API token for{` `}
<strong>{state.selectedServiceAccount?.name}</strong>?
Expand All @@ -207,7 +210,7 @@ const App = () => {
Any systems using the old token will lose access.
</Box>
</div>
<div className="flex justify-end items-center pa4 bt b--black-10">
<div className="flex justify-end items-center pa3 bt b--black-10">
<button
className="btn btn-secondary mr3"
onClick={() => setRegenerateModalOpen(false)}
Expand Down
Loading