Skip to content
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

Users can now spectate via basic web ui #68

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ dependencies {
implementation("io.quarkus:quarkus-info")
implementation("io.quarkus:quarkus-kotlin")
implementation("io.quarkus:quarkus-resteasy-reactive")
implementation("io.quarkus:quarkus-rest-qute")
implementation("io.quarkiverse.qute.web:quarkus-qute-web")
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
implementation(libs.jackson.kotlin)
implementation("io.quarkus:quarkus-websockets")
Expand Down
19 changes: 18 additions & 1 deletion api/src/main/kotlin/pp/api/RoomsResource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@
import io.quarkus.logging.Log
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType.APPLICATION_JSON
import jakarta.ws.rs.core.Response
import jakarta.ws.rs.core.Response.temporaryRedirect
import jakarta.ws.rs.core.UriInfo
import pp.api.dto.RoomDto
import java.net.URI
import java.net.URLEncoder.encode
import java.nio.charset.StandardCharsets.UTF_8

/**
* List and join rooms
*
* @param rooms
*/
@Path("/rooms")
class RoomsResource {
class RoomsResource(
private val rooms: Rooms,

Check warning on line 23 in api/src/main/kotlin/pp/api/RoomsResource.kt

View check run for this annotation

Codecov / codecov/patch

api/src/main/kotlin/pp/api/RoomsResource.kt#L22-L23

Added lines #L22 - L23 were not covered by tests
) {
/*
* Portions of this file are derived from work licensed under the Apache License, Version 2.0
* See the original file at http://www.apache.org/licenses/LICENSE-2.0
Expand Down Expand Up @@ -146,6 +153,7 @@
* @return a HTTP 307 temporary redirect with the websocket URL in the `Location` header
*/
@Path("new")
@Produces(APPLICATION_JSON)
@GET
fun createRandomRoom(uri: UriInfo): Response {
val roomId = saoItems.random() + " " + locations.random()
Expand All @@ -156,4 +164,13 @@
Log.info("Redirecting to $newUri")
return temporaryRedirect(newUri).build()
}

/**
* Get a JSON representation of all rooms
*
* @return all [pp.api.data.Room]s as [RoomDto]s
*/
@GET
@Produces(APPLICATION_JSON)
fun getRooms(): List<RoomDto> = rooms.getRooms().sortedBy { it.roomId }.map { RoomDto(it) }

Check warning on line 175 in api/src/main/kotlin/pp/api/RoomsResource.kt

View check run for this annotation

Codecov / codecov/patch

api/src/main/kotlin/pp/api/RoomsResource.kt#L175

Added line #L175 was not covered by tests
}
29 changes: 29 additions & 0 deletions api/src/main/kotlin/pp/api/SingleRoomResource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package pp.api

import io.quarkus.qute.Template
import io.quarkus.qute.TemplateInstance
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType.TEXT_HTML

/**
* Renders a HTML page to display a single room
*
* @param room
*/
@Path("/room/{roomId}")
class SingleRoomResource(
private val room: Template,
) {
/**
* Renders a HTML page to display a single room
*
* @param roomId id of the room to display
* @return a [TemplateInstance] rendering this room
*/
@GET
@Produces(TEXT_HTML)
fun get(@PathParam("roomId") roomId: String): TemplateInstance = room.data("roomId", roomId)
}
23 changes: 13 additions & 10 deletions api/src/main/kotlin/pp/api/Util.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ import java.nio.charset.StandardCharsets
* @param query the query string
* @return parsed params in a [Map] `key -> value`
*/
fun parseQuery(query: String): Map<String, String> = query.split("&")
.filter { it.contains("=") }
.mapNotNull {
val parts = it.split("=")
if (parts[0].trim().isNotBlank() && parts[1].trim().isNotBlank()) {
parts[0].trim() to decode(parts[1].trim(), StandardCharsets.UTF_8)
} else {
null
fun parseQuery(query: String?): Map<String, String> {
query ?: return emptyMap()
return query.split("&")
.filter { it.contains("=") }
.mapNotNull {
val parts = it.split("=")
if (parts[0].trim().isNotBlank() && parts[1].trim().isNotBlank()) {
parts[0].trim() to decode(parts[1].trim(), StandardCharsets.UTF_8)
} else {
null
}
}
}
.toMap()
.toMap()
}
8 changes: 8 additions & 0 deletions api/src/main/kotlin/pp/api/data/Room.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pp.api.data
import io.quarkus.logging.Log
import jakarta.websocket.Session
import pp.api.data.GamePhase.PLAYING
import pp.api.data.UserType.PARTICIPANT

/**
* A planning poker room.
Expand All @@ -27,6 +28,13 @@ class Room(
val gamePhase: GamePhase = PLAYING,
val log: List<LogEntry> = emptyList(),
) {
/**
* A list of all participants in this room.
*
* That is, all users with [User.userType] == [UserType.PARTICIPANT]
*/
val participants: List<User> = users.filter { it.userType == PARTICIPANT }

/**
* Helper function to create a new room, based on the current one
*
Expand Down
6 changes: 5 additions & 1 deletion api/src/main/kotlin/pp/api/data/User.kt
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,11 @@ data class User(
*/
constructor(session: Session) : this(
username = parseQuery(session.queryString)["user"] ?: funnyNames.random(),
userType = SPECTATOR,
userType = parseQuery(session.queryString)["userType"].let { typeString ->
typeString?.let {
UserType.valueOf(typeString)
} ?: SPECTATOR
},
cardValue = null,
session = session
)
Expand Down
12 changes: 7 additions & 5 deletions api/src/main/kotlin/pp/api/dto/RoomDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,20 @@ data class RoomDto(
val average: String,
val log: List<LogEntry>,
) {
constructor(room: Room, yourUser: User) : this(
constructor(room: Room, yourUser: User? = null) : this(
roomId = room.roomId,
deck = room.deck,
gamePhase = room.gamePhase,
users = room.users.map { user ->
UserDto(user, isYourUser = user == yourUser, room.gamePhase)
},
}.sortedBy { it.username },
average = (if (room.gamePhase == CARDS_REVEALED) {
if (1 == room.users.groupBy { it.cardValue }.size && room.users.first().cardValue != null) {
room.users.first().cardValue!!
if (1 == room.participants
.groupBy { it.cardValue }.size && room.users.first().cardValue != null
) {
room.participants.first().cardValue!!
} else {
val hasSomeNoInt = room.users.any { it.cardValue?.toIntOrNull() == null }
val hasSomeNoInt = room.participants.any { it.cardValue?.toIntOrNull() == null }
room.users
.mapNotNull {
it.cardValue?.toIntOrNull()
Expand Down
5 changes: 4 additions & 1 deletion api/src/main/kotlin/pp/api/dto/UserDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import pp.api.data.GamePhase
import pp.api.data.GamePhase.CARDS_REVEALED
import pp.api.data.User
import pp.api.data.UserType
import pp.api.data.UserType.SPECTATOR

/**
* User representation for clients
Expand All @@ -28,7 +29,9 @@ data class UserDto(
username = user.username,
userType = user.userType,
isYourUser = isYourUser,
cardValue = if (gamePhase == CARDS_REVEALED || isYourUser) {
cardValue = if (user.userType == SPECTATOR) {
""
} else if (gamePhase == CARDS_REVEALED || isYourUser) {
user.cardValue ?: ""
} else {
user.cardValue?.let {
Expand Down
35 changes: 16 additions & 19 deletions api/src/main/resources/META-INF/resources/index.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pp</title>
</head>
<script>

function joinroom() {
const id = document.getElementById('room').value
console.log('room id', id)
const socket = new WebSocket("ws://localhost:8080/rooms/" + id);
socket.addEventListener("open", (event) => {
});

socket.addEventListener("message", (event) => {
console.log(event.data);
});
socket.addEventListener("error", (event) => {
console.error("error ", event);
});

}
onload = async () => {
const rooms = await (await fetch('/rooms')).json()
const roomsElem = document.getElementById('rooms')
roomsElem.innerHTML = ''
rooms.forEach(r => {
const item = document.createElement('li')
const link = document.createElement('a')
link.innerText = r.roomId
link.setAttribute('href', '/room/' + encodeURIComponent(r.roomId))
item.appendChild(link)
roomsElem.appendChild(item)
})
};
</script>
<input type="text" id="room">
<button onclick="joinroom()">join</button>
Click on a room show view the voting process
<ul id="rooms"></ul>
</html>
45 changes: 45 additions & 0 deletions api/src/main/resources/templates/room.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>pp - {roomId}</title>
</head>
<script>
onload = async () => {
const wsUrl = location.toString().replace('http', 'ws').replace('/room/', '/rooms/')
const socket = new WebSocket(wsUrl);
socket.addEventListener("message", (event) => {
const room = JSON.parse(event.data)
const userList = document.querySelectorAll('#users tbody')[0]
userList.innerHTML = ''
room.users
.filter(u => u.userType !== 'SPECTATOR')
.forEach(user => {
const row = document.createElement('tr')
const tdu = document.createElement('td')
tdu.innerText = user.username
row.appendChild(tdu)
const tdc = document.createElement('td')
tdc.innerText = user.cardValue
row.appendChild(tdc)
userList.appendChild(row)
})
document.getElementById('gamePhase').innerText = room.gamePhase === 'PLAYING' ? 'Playing' : 'Showing cards'
document.getElementById('average').innerText = room.average
});
};
</script>
<h1>{roomId} &ndash; <span id="gamePhase"></span></h1>
<h2>Users</h2>
<table id="users">
<thead>
<tr>
<th>Username</th>
<th>Card</th>
</tr>
</thead>
<tbody></tbody>
</table>
<h2>Result</h2>
<span id="average"></span>
</html>
54 changes: 54 additions & 0 deletions api/src/test/kotlin/pp/api/RoomsResourceTest.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
package pp.api

import io.quarkus.test.InjectMock
import io.quarkus.test.common.http.TestHTTPEndpoint
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured.given
import io.restassured.http.ContentType.JSON
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.matchesPattern
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import org.mockito.kotlin.whenever
import pp.api.data.Room
import pp.api.data.User
import pp.api.data.UserType.PARTICIPANT

/**
* Test the [RoomsResource]
*/
@QuarkusTest
@TestHTTPEndpoint(RoomsResource::class)
class RoomsResourceTest {
@InjectMock
private lateinit var rooms: Rooms

@Test
fun testCreateRandomRoom() {
val locationRegex =
Expand All @@ -25,4 +36,47 @@ class RoomsResourceTest {
.statusCode(307)
.header("Location", matchesPattern(locationRegex))
}

@Test
fun getRoomsEmpty() {
whenever(rooms.getRooms()).thenReturn(emptySet())
given()
.get()
.then()
.statusCode(200)
.contentType(JSON)
.body(equalTo("[]"))
}

@Test
fun getRoomsWithUser() {
whenever(rooms.getRooms()).thenReturn(
setOf(
Room(
roomId = "roomId",
users = listOf(
User(
username = "username",
userType = PARTICIPANT,
cardValue = "19",
session = mock(),
)
)
)
)
)

given()
.get()
.then()
.statusCode(200)
.contentType(JSON)
.body(
equalTo(
"""[{"roomId":"roomId","deck":["1","2","3","5","8","13","☕"],
|"gamePhase":"PLAYING","users":[{"username":"username","userType":"PARTICIPANT",
|"isYourUser":false,"cardValue":"✅"}],"average":"?","log":[]}]""".trimMargin().replace("\n", "")
)
)
}
}
26 changes: 26 additions & 0 deletions api/src/test/kotlin/pp/api/SingleRoomResourceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package pp.api

import io.quarkus.test.InjectMock
import io.quarkus.test.common.http.TestHTTPEndpoint
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured.given
import org.hamcrest.Matchers.containsString
import org.junit.jupiter.api.Test
import org.mockito.kotlin.whenever

@QuarkusTest
@TestHTTPEndpoint(SingleRoomResource::class)
class SingleRoomResourceTest {
@InjectMock
private lateinit var rooms: Rooms

@Test
fun get() {
whenever(rooms.getRooms()).thenReturn(emptySet())
given()
.get("", "nice-room-id")
.then()
.statusCode(200)
.body(containsString("<title>pp - nice-room-id</title>"))
}
}
Loading