Skip to content

Commit

Permalink
Add user list page (#965)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliver3 authored Feb 6, 2025
1 parent bb61e4b commit be7ad06
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 30 deletions.
2 changes: 1 addition & 1 deletion backend/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1049,7 +1049,7 @@
"authentication"
],
"summary": "Lists all users",
"operationId": "list",
"operationId": "user_list",
"responses": {
"200": {
"description": "User list",
Expand Down
4 changes: 3 additions & 1 deletion backend/src/authentication/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ pub struct UserListResponse {
(status = 500, description = "Internal server error", body = ErrorResponse),
),
)]
pub async fn list(State(users_repo): State<Users>) -> Result<Json<UserListResponse>, APIError> {
pub async fn user_list(
State(users_repo): State<Users>,
) -> Result<Json<UserListResponse>, APIError> {
Ok(Json(UserListResponse {
users: users_repo.list().await?,
}))
Expand Down
2 changes: 1 addition & 1 deletion backend/src/authentication/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ mod tests {
let state = AppState { pool };

let router = Router::new()
.route("/api/user", get(api::list))
.route("/api/user", get(api::user_list))
.route("/api/user/login", post(api::login))
.route("/api/user/logout", post(api::logout))
.route("/api/user/whoami", get(api::whoami))
Expand Down
4 changes: 2 additions & 2 deletions backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ pub fn router(pool: SqlitePool) -> Result<Router, Box<dyn Error>> {
let election_routes = election_routes.route("/", post(election::election_create));

let user_router = Router::new()
.route("/", get(authentication::list))
.route("/", get(authentication::user_list))
.route("/login", post(authentication::login))
.route("/logout", post(authentication::logout))
.route("/whoami", get(authentication::whoami))
Expand Down Expand Up @@ -154,7 +154,7 @@ pub fn create_openapi() -> utoipa::openapi::OpenApi {
authentication::logout,
authentication::whoami,
authentication::change_password,
authentication::list,
authentication::user_list,
election::election_list,
election::election_create,
election::election_details,
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/module/DevHomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function DevLinks() {
))}
</ul>
<li>
<Link to={`/users#administratorcoordinator`}>{t("user.manage")}</Link>
<Link to={`/users#administratorcoordinator`}>{t("user.management")}</Link>
</li>
<li>
<Link to={`/workstations#administrator`}>{t("workstations.manage")}</Link>
Expand Down
28 changes: 28 additions & 0 deletions frontend/app/module/users/UserListPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, test } from "vitest";

import { UserListRequestHandler } from "@kiesraad/api-mocks";
import { render, server } from "@kiesraad/test";

import { UserListPage } from "./UserListPage";

describe("PollingStationListPage", () => {
beforeEach(() => {
server.use(UserListRequestHandler);
});

test("Show users", async () => {
render(<UserListPage />);

const table = await screen.findByRole("table");
expect(table).toBeVisible();
expect(table).toHaveTableContent([

Check failure on line 19 in frontend/app/module/users/UserListPage.test.tsx

View workflow job for this annotation

GitHub Actions / Frontend

app/module/users/UserListPage.test.tsx > PollingStationListPage > Show users

Error: Expected table to have content: [["Gebruikersnaam","Rol","Volledige naam","Laatste activiteit"],["Sanne","Beheerder","Sanne Molenaar","vandaag 10:20"],["Jayden","Coördinator","Jayden Ahmen","dinsdag 10:20"],["Gebruiker01","Invoerder","Nog niet gebruikt","–"],["Gebruiker02","Invoerder","Nog niet gebruikt","–"],["Gebruiker03","Invoerder","Nog niet gebruikt","–"]], but got: [["Gebruikersnaam","Rol","Volledige naam","Laatste activiteit"],["Sanne","Beheerder","Sanne Molenaar","vandaag 10:20"],["Jayden","Coördinator","Jayden Ahmen","woensdag 10:20"],["Gebruiker01","Invoerder","Nog niet gebruikt","–"],["Gebruiker02","Invoerder","Nog niet gebruikt","–"],["Gebruiker03","Invoerder","Nog niet gebruikt","–"]] - Expected + Received [ [ "Gebruikersnaam", "Rol", "Volledige naam", "Laatste activiteit", ], [ "Sanne", "Beheerder", "Sanne Molenaar", "vandaag 10:20", ], [ "Jayden", "Coördinator", "Jayden Ahmen", - "dinsdag 10:20", + "woensdag 10:20", ], [ "Gebruiker01", "Invoerder", "Nog niet gebruikt", "–", ], [ "Gebruiker02", "Invoerder", "Nog niet gebruikt", "–", ], [ "Gebruiker03", "Invoerder", "Nog niet gebruikt", "–", ], ] ❯ app/module/users/UserListPage.test.tsx:19:19
["Gebruikersnaam", "Rol", "Volledige naam", "Laatste activiteit"],
["Sanne", "Beheerder", "Sanne Molenaar", "vandaag 10:20"],
["Jayden", "Coördinator", "Jayden Ahmen", "dinsdag 10:20"],
["Gebruiker01", "Invoerder", "Nog niet gebruikt", "–"],
["Gebruiker02", "Invoerder", "Nog niet gebruikt", "–"],
["Gebruiker03", "Invoerder", "Nog niet gebruikt", "–"],
]);
});
});
54 changes: 54 additions & 0 deletions frontend/app/module/users/UserListPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useUserListRequest } from "app/module/users/useUserListRequest";

import { t } from "@kiesraad/i18n";
import { Loader, PageTitle, Table } from "@kiesraad/ui";
import { formatDateTime } from "@kiesraad/util";

export function UserListPage() {
const { requestState } = useUserListRequest();

if (requestState.status === "loading") {
return <Loader />;
}

if ("error" in requestState) {
throw requestState.error;
}

const users = requestState.data.users;

return (
<>
<PageTitle title={`${t("user.management")} - Abacus`} />
<header>
<section>
<h1>{t("user.management")}</h1>
</section>
</header>
<main>
<article>
<Table id="users">
<Table.Header>
<Table.Column>{t("user.username")}</Table.Column>
<Table.Column>{t("role")}</Table.Column>
<Table.Column>{t("user.fullname")}</Table.Column>
<Table.Column>{t("user.last_activity")}</Table.Column>
</Table.Header>
<Table.Body className="fs-md">
{users.map((user) => (
<Table.LinkRow key={user.id} to={`${user.id}/update`}>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{t(user.role)}</Table.Cell>
<Table.Cell>{user.fullname ?? <span className="text-muted">{t("user.not_used")}</span>}</Table.Cell>
<Table.Cell>
{user.last_activity_at ? formatDateTime(new Date(user.last_activity_at)) : "–"}
</Table.Cell>
</Table.LinkRow>
))}
</Table.Body>
</Table>
</article>
</main>
</>
);
}
18 changes: 0 additions & 18 deletions frontend/app/module/users/UsersHomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +0,0 @@
import { t } from "@kiesraad/i18n";
import { PageTitle } from "@kiesraad/ui";

export function UsersHomePage() {
return (
<>
<PageTitle title={`${t("user.management")} - Abacus`} />
<header>
<section>
<h1>{t("user.manage")}</h1>
</section>
</header>
<main>
<article>Placeholder</article>
</main>
</>
);
}
2 changes: 1 addition & 1 deletion frontend/app/module/users/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from "./UsersHomePage";
export * from "./UserListPage";
6 changes: 6 additions & 0 deletions frontend/app/module/users/useUserListRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useApiRequestWithErrors, USER_LIST_REQUEST_PATH, UserListResponse } from "@kiesraad/api";

export function useUserListRequest() {
const path: USER_LIST_REQUEST_PATH = `/api/user`;
return useApiRequestWithErrors<UserListResponse>(path);
}
6 changes: 4 additions & 2 deletions frontend/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { LogsHomePage } from "app/module/logs";
import { NotAvailableInMock } from "app/module/NotAvailableInMock";
import { PollingStationListPage, PollingStationsLayout } from "app/module/polling_stations";
import { UsersHomePage } from "app/module/users";
import { UserListPage } from "app/module/users";
import { WorkstationsHomePage } from "app/module/workstations";

import { t } from "@kiesraad/i18n";
Expand Down Expand Up @@ -82,7 +82,9 @@ export const routes = createRoutesFromElements(
<Route element={<AdministratorLayout />}>
<Route path="dev" element={<DevHomePage />} />
<Route path="logs" element={<LogsHomePage />} />
<Route path="users" element={<UsersHomePage />} />
<Route path="users">
<Route index element={<UserListPage />} />
</Route>
<Route path="workstations" element={<WorkstationsHomePage />} />
</Route>
</Route>,
Expand Down
12 changes: 12 additions & 0 deletions frontend/lib/api-mocks/RequestHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ import {
PollingStation,
PollingStationListResponse,
SaveDataEntryResponse,
USER_LIST_REQUEST_PARAMS,
USER_LIST_REQUEST_PATH,
UserListResponse,
} from "@kiesraad/api";

import { electionDetailsMockResponse, electionListMockResponse, electionStatusMockResponse } from "./ElectionMockData";
import { pollingStationMockData } from "./PollingStationMockData";
import { userMockData } from "./UserMockData";

type ParamsToString<T> = {
[P in keyof T]: string;
Expand Down Expand Up @@ -117,6 +121,13 @@ export const PollingStationGetHandler = http.get<ParamsToString<POLLING_STATION_
() => HttpResponse.json(pollingStationMockData[0]! satisfies PollingStation, { status: 200 }),
);

export const UserListRequestHandler = http.get<
USER_LIST_REQUEST_PARAMS,
null,
UserListResponse,
USER_LIST_REQUEST_PATH
>("/api/user", () => HttpResponse.json({ users: userMockData }, { status: 200 }));

export const handlers: HttpHandler[] = [
pingHandler,
WhoAmIRequestHandler,
Expand All @@ -131,4 +142,5 @@ export const handlers: HttpHandler[] = [
PollingStationCreateHandler,
PollingStationGetHandler,
PollingStationUpdateHandler,
UserListRequestHandler,
];
52 changes: 52 additions & 0 deletions frontend/lib/api-mocks/UserMockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ListedUser } from "@kiesraad/api";

const today = new Date();
today.setHours(10, 20);

const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);

const created_at = new Date().toISOString();
const updated_at = new Date().toISOString();

export const userMockData: ListedUser[] = [
{
id: 1,
username: "Sanne",
role: "administrator",
fullname: "Sanne Molenaar",
last_activity_at: today.toISOString(),
created_at,
updated_at,
},
{
id: 2,
username: "Jayden",
role: "coordinator",
fullname: "Jayden Ahmen",
last_activity_at: yesterday.toISOString(),
created_at,
updated_at,
},
{
id: 3,
username: "Gebruiker01",
role: "typist",
created_at,
updated_at,
},
{
id: 4,
username: "Gebruiker02",
role: "typist",
created_at,
updated_at,
},
{
id: 5,
username: "Gebruiker03",
role: "typist",
created_at,
updated_at,
},
];
4 changes: 2 additions & 2 deletions frontend/lib/api/gen/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ export type POLLING_STATION_DATA_ENTRY_FINALISE_REQUEST_PATH =
`/api/polling_stations/${number}/data_entries/${number}/finalise`;

// /api/user
export type LIST_REQUEST_PARAMS = Record<string, never>;
export type LIST_REQUEST_PATH = `/api/user`;
export type USER_LIST_REQUEST_PARAMS = Record<string, never>;
export type USER_LIST_REQUEST_PATH = `/api/user`;

// /api/user/change-password
export type CHANGE_PASSWORD_REQUEST_PARAMS = Record<string, never>;
Expand Down
4 changes: 3 additions & 1 deletion frontend/lib/i18n/locales/nl/user.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"personalize_account": "Personaliseer je account",
"username": "Gebruikersnaam",
"fullname": "Volledige naam",
"last_activity": "Laatste activiteit",
"not_used": "Nog niet gebruikt",
"username_hint": "Je kan deze niet aanpassen. Log volgende keer weer met deze gebruikersnaam in.",
"username_login_hint": "De naam op het briefje dat je van de coördinator hebt gekregen.",
"username_default": "Gebruiker01",
"manage": "Gebruikers beheren",
"management": "Gebruikersbeheer",
"name": "Jouw naam",
"name_subtext": "(roepnaam + achternaam)",
Expand Down
4 changes: 4 additions & 0 deletions frontend/lib/ui/style/util.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@
background-color: var(--bg-gray);
}

.text-muted {
color: var(--gray-400);
}

/* Width */
.w-13 {
width: 13rem;
Expand Down

0 comments on commit be7ad06

Please sign in to comment.