Skip to content

Commit

Permalink
Merge pull request #6172 from espoon-voltti/filtering-users-by-roles
Browse files Browse the repository at this point in the history
Käyttäjien haku roolien mukaan
  • Loading branch information
Joosakur authored Dec 20, 2024
2 parents 6883d94 + 436e0e3 commit 82c49dc
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 95 deletions.
135 changes: 75 additions & 60 deletions frontend/src/employee-frontend/components/employees/EmployeesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,40 @@
import React, { useState } from 'react'
import styled from 'styled-components'

import { globalRoles } from 'lib-common/api-types/employee-auth'
import { UserRole } from 'lib-common/generated/api-types/shared'
import { useQueryResult } from 'lib-common/query'
import { useDebounce } from 'lib-common/utils/useDebounce'
import Pagination from 'lib-components/Pagination'
import Title from 'lib-components/atoms/Title'
import Checkbox from 'lib-components/atoms/form/Checkbox'
import InputField from 'lib-components/atoms/form/InputField'
import { Container, ContentArea } from 'lib-components/layout/Container'
import { FixedSpaceRow } from 'lib-components/layout/flex-helpers'
import { defaultMargins } from 'lib-components/white-space'
import {
FixedSpaceColumn,
FixedSpaceFlexWrap,
FixedSpaceRow
} from 'lib-components/layout/flex-helpers'
import { Label } from 'lib-components/typography'
import { faSearch } from 'lib-icons'

import { useTranslation } from '../../state/i18n'
import { renderResult } from '../async-rendering'
import { FlexRow } from '../common/styled/containers'

import { EmployeeList } from './EmployeeList'
import { searchEmployeesQuery } from './queries'

const TopBar = styled.section`
display: flex;
justify-content: space-between;
margin-bottom: ${defaultMargins.s};
`

const SearchBar = styled.div`
display: flex;
justify-content: space-between;
align-items: flex-start;
`

const PaginationContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
const ResultCount = styled.div`
text-wrap: nowrap;
`

export default React.memo(function EmployeesPage() {
const { i18n } = useTranslation()
const [page, setPage] = useState<number>(1)
const [searchTerm, setSearchTerm] = useState<string>('')
const [hideDeactivated, setHideDeactivated] = useState<boolean>(false)
const [selectedGlobalRoles, setSelectedGlobalRoles] = useState<UserRole[]>([])

const debouncedSearchTerm = useDebounce(searchTerm, 500)

Expand All @@ -54,7 +47,8 @@ export default React.memo(function EmployeesPage() {
body: {
page,
searchTerm: debouncedSearchTerm,
hideDeactivated
hideDeactivated,
globalRoles: selectedGlobalRoles
}
})
)
Expand All @@ -63,58 +57,79 @@ export default React.memo(function EmployeesPage() {
<Container>
<ContentArea opaque>
<Title>{i18n.titles.employees}</Title>
<TopBar>
<SearchBar>
<FixedSpaceRow>
<InputField
data-qa="employee-name-filter"
value={searchTerm}
placeholder={i18n.employees.findByName}
onChange={(s) => {
setSearchTerm(s)
setPage(1)
}}
icon={faSearch}
width="L"
<FixedSpaceColumn spacing="m">
<InputField
data-qa="employee-name-filter"
value={searchTerm}
placeholder={i18n.employees.findByName}
onChange={(s) => {
setSearchTerm(s)
setPage(1)
}}
icon={faSearch}
width="L"
/>
<Checkbox
label={i18n.employees.hideDeactivated}
checked={hideDeactivated}
onChange={(enabled) => {
setHideDeactivated(enabled)
setPage(1)
}}
data-qa="hide-deactivated-checkbox"
/>
<FixedSpaceRow
justifyContent="space-between"
alignItems="center"
spacing="XL"
>
<FixedSpaceColumn spacing="xs">
<Label>{i18n.employees.editor.globalRoles}</Label>
<FixedSpaceFlexWrap horizontalSpacing="m">
{globalRoles.map((role) => (
<Checkbox
key={role}
label={i18n.roles.adRoles[role]}
checked={selectedGlobalRoles.includes(role)}
onChange={(checked) =>
setSelectedGlobalRoles(
checked
? [...selectedGlobalRoles, role]
: selectedGlobalRoles.filter((r) => r !== role)
)
}
/>
))}
</FixedSpaceFlexWrap>
</FixedSpaceColumn>
<FixedSpaceColumn spacing="xxs" alignItems="flex-end">
<Pagination
pages={employees.map((res) => res.pages).getOrElse(1)}
currentPage={page}
setPage={setPage}
label={i18n.common.page}
/>
<ResultCount>
{employees.isSuccess
? i18n.common.resultCount(employees.value.total)
: null}
</ResultCount>
</FixedSpaceColumn>
</FixedSpaceRow>
</FixedSpaceColumn>

<Checkbox
label={i18n.employees.hideDeactivated}
checked={hideDeactivated}
onChange={(enabled) => {
setHideDeactivated(enabled)
setPage(1)
}}
data-qa="hide-deactivated-checkbox"
/>
</FixedSpaceRow>
</SearchBar>
<PaginationContainer>
<div>
{employees.isSuccess
? i18n.common.resultCount(employees.value.total)
: null}
</div>
<Pagination
pages={employees.map((res) => res.pages).getOrElse(1)}
currentPage={page}
setPage={setPage}
label={i18n.common.page}
/>
</PaginationContainer>
</TopBar>
{renderResult(employees, (employees) => (
<EmployeeList employees={employees.data} />
))}
{employees?.isSuccess && (
<PaginationContainer>
<FlexRow justifyContent="flex-end">
<Pagination
pages={employees.value.pages}
currentPage={page}
setPage={setPage}
label={i18n.common.page}
/>
</PaginationContainer>
</FlexRow>
)}
</ContentArea>
</Container>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ import { createQueryKeys } from '../../query'

export const queryKeys = createQueryKeys('employees', {
searchAll: () => ['search'],
search: (
page: number | null,
searchTerm: string | null,
hideDeactivated: boolean | null
) => ['search', page, searchTerm, hideDeactivated],
search: (args: Arg0<typeof searchEmployees>) => ['search', args],
byId: (id: UUID) => ['id', id]
})

export const searchEmployeesQuery = query({
api: searchEmployees,
queryKey: ({ body: { page, searchTerm, hideDeactivated } }) =>
queryKeys.search(page, searchTerm, hideDeactivated)
queryKey: queryKeys.search
})

export const employeeDetailsQuery = query({
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib-common/generated/api-types/pis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ export interface RestrictedDetails {
* Generated from fi.espoo.evaka.pis.controllers.SearchEmployeeRequest
*/
export interface SearchEmployeeRequest {
globalRoles: UserRole[] | null
hideDeactivated: boolean | null
page: number | null
searchTerm: string | null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4585,7 +4585,7 @@ export const fi = {
activateConfirm: 'Haluatko palauttaa käyttäjän aktiiviseksi?',
deactivate: 'Deaktivoi',
deactivateConfirm: 'Haluatko deaktivoida käyttäjän?',
hideDeactivated: 'Näytä vain aktiiviset',
hideDeactivated: 'Näytä vain aktiiviset käyttäjät',
editor: {
globalRoles: 'Järjestelmäroolit',
unitRoles: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import fi.espoo.evaka.testDecisionMaker_3
import fi.espoo.evaka.unitSupervisorOfTestDaycare
import java.util.UUID
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.fail
Expand Down Expand Up @@ -53,7 +54,12 @@ class EmployeeControllerSearchIntegrationTest : FullApplicationTest(resetDbBefor
dbInstance(),
user,
RealEvakaClock(),
SearchEmployeeRequest(page = 1, searchTerm = null, hideDeactivated = false),
SearchEmployeeRequest(
page = 1,
searchTerm = null,
hideDeactivated = false,
globalRoles = emptySet(),
),
)

assertEquals(4, body.total)
Expand Down Expand Up @@ -88,10 +94,34 @@ class EmployeeControllerSearchIntegrationTest : FullApplicationTest(resetDbBefor
dbInstance(),
user,
RealEvakaClock(),
SearchEmployeeRequest(page = 1, searchTerm = "super", hideDeactivated = false),
SearchEmployeeRequest(
page = 1,
searchTerm = "super",
hideDeactivated = false,
globalRoles = emptySet(),
),
)
assertEquals(1, body.data.size)
assertEquals("Sammy", body.data[0].firstName)
assertEquals("Supervisor", body.data[0].lastName)
}

@Test
fun `admin searches employees with roles`() {
val user = AuthenticatedUser.Employee(EmployeeId(UUID.randomUUID()), setOf(UserRole.ADMIN))
val body =
controller.searchEmployees(
dbInstance(),
user,
RealEvakaClock(),
SearchEmployeeRequest(
page = 1,
searchTerm = null,
hideDeactivated = false,
globalRoles = setOf(UserRole.SERVICE_WORKER),
),
)
assertEquals(1, body.data.size)
assertTrue { body.data.all { it.globalRoles.toSet().contains(UserRole.SERVICE_WORKER) } }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ fun getEmployeesPaged(
pageSize: Int,
searchTerm: String = "",
hideDeactivated: Boolean = false,
globalRoles: Set<UserRole>?,
): PagedEmployeesWithDaycareRoles {
val (freeTextQuery, freeTextParams) =
freeTextSearchQueryForColumns(
Expand All @@ -387,12 +388,17 @@ fun getEmployeesPaged(
)

val params =
listOf(Binding.of("offset", (page - 1) * pageSize), Binding.of("pageSize", pageSize))
listOfNotNull(
Binding.of("offset", (page - 1) * pageSize),
Binding.of("pageSize", pageSize),
if (globalRoles != null) Binding.of("roles", globalRoles) else null,
)

val conditions =
listOfNotNull(
if (searchTerm.isNotBlank()) freeTextQuery else null,
if (hideDeactivated) "employee.active = TRUE" else null,
if (globalRoles != null) "employee.roles && :roles" else null,
)

val whereClause = conditions.takeIf { it.isNotEmpty() }?.joinToString(" AND ") ?: "TRUE"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ class EmployeeController(private val accessControl: AccessControl) {
@PathVariable id: EmployeeId,
@RequestBody body: List<UserRole>,
) {
body.forEach { role ->
if (!role.isGlobalRole()) {
throw BadRequest("Role $role is not a global role")
}
}

db.connect { dbc ->
dbc.transaction {
accessControl.requirePermissionFor(
Expand Down Expand Up @@ -330,11 +336,16 @@ class EmployeeController(private val accessControl: AccessControl) {
Action.Global.SEARCH_EMPLOYEES,
)
getEmployeesPaged(
tx,
body.page ?: 1,
tx = tx,
page = body.page ?: 1,
pageSize = 50,
body.searchTerm ?: "",
body.hideDeactivated ?: false,
searchTerm = body.searchTerm ?: "",
hideDeactivated = body.hideDeactivated ?: false,
globalRoles =
body.globalRoles
?.takeIf { it.isNotEmpty() }
?.filter { it.isGlobalRole() }
?.toSet(),
)
}
}
Expand Down Expand Up @@ -403,4 +414,5 @@ data class SearchEmployeeRequest(
val page: Int?,
val searchTerm: String?,
val hideDeactivated: Boolean?,
val globalRoles: Set<UserRole>?,
)
24 changes: 4 additions & 20 deletions service/src/main/kotlin/fi/espoo/evaka/shared/auth/UserRole.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,9 @@ enum class UserRole : DatabaseEnum {
/** Varhaiskasvatussihteeri */
EARLY_CHILDHOOD_EDUCATION_SECRETARY;

fun isGlobalRole(): Boolean =
when (this) {
ADMIN -> true
REPORT_VIEWER -> true
DIRECTOR -> true
FINANCE_ADMIN -> true
SERVICE_WORKER -> true
MESSAGING -> true
FINANCE_STAFF -> true
else -> false
}

fun isUnitScopedRole(): Boolean =
when (this) {
UNIT_SUPERVISOR -> true
STAFF -> true
SPECIAL_EDUCATION_TEACHER -> true
EARLY_CHILDHOOD_EDUCATION_SECRETARY -> true
else -> false
}
fun isGlobalRole(): Boolean = GLOBAL_ROLES.contains(this)

fun isUnitScopedRole(): Boolean = SCOPED_ROLES.contains(this)

override val sqlType: String = "user_role"

Expand All @@ -52,5 +35,6 @@ enum class UserRole : DatabaseEnum {
SPECIAL_EDUCATION_TEACHER,
EARLY_CHILDHOOD_EDUCATION_SECRETARY,
)
val GLOBAL_ROLES = entries.filter { !SCOPED_ROLES.contains(it) }.toSet()
}
}

0 comments on commit 82c49dc

Please sign in to comment.