Skip to content

Demo search on frontend #2058

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

Merged
merged 3 commits into from
Mar 31, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ class ProjectService(
* @return project's with filter
*/
fun getFiltered(projectFilter: ProjectFilter): List<Project> = projectRepository.findAll { root, _, cb ->

val publicPredicate = projectFilter.public?.let { cb.equal(root.get<Boolean>("public"), it) } ?: cb.and()
val orgNamePredicate = if (projectFilter.organizationName.isBlank()) {
cb.and()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ data class DemoDto(
fun getAvailableMods(): List<String> = runCommands.keys.toList()

companion object {
/**
* Amount of [DemoDto]s that should be fetched by default
*/
const val DEFAULT_FETCH_NUMBER = 10
val empty = emptyForProject("", "")

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.saveourtool.save.filters

import com.saveourtool.save.demo.DemoStatus
import kotlinx.serialization.Serializable

/**
* @property organizationName substring that should match saveourtool organization name
* @property projectName substring that should match saveourtool project name
* @property statuses
*/
@Serializable
data class DemoFilter(
val organizationName: String,
val projectName: String,
val statuses: Set<DemoStatus>
) {
companion object {
/**
* The filter which returns all the demos
*/
val any = DemoFilter(
organizationName = "",
projectName = "",
statuses = DemoStatus.values().filter { it != DemoStatus.NOT_CREATED }.toSet()
)

/**
* The filter which returns all active demos
*/
val running = DemoFilter(
organizationName = "",
projectName = "",
statuses = setOf(DemoStatus.RUNNING)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import com.saveourtool.save.configs.ApiSwaggerSupport
import com.saveourtool.save.demo.DemoDto
import com.saveourtool.save.demo.DemoResult
import com.saveourtool.save.demo.DemoRunRequest
import com.saveourtool.save.demo.DemoStatus
import com.saveourtool.save.demo.runners.RunnerFactory
import com.saveourtool.save.demo.service.DemoService
import com.saveourtool.save.filters.DemoFilter
import com.saveourtool.save.utils.blockingToFlux
import io.swagger.v3.oas.annotations.tags.Tag
import io.swagger.v3.oas.annotations.tags.Tags
Expand All @@ -15,9 +15,11 @@ import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.kotlin.core.publisher.toMono

import reactor.kotlin.core.util.function.component1
import reactor.kotlin.core.util.function.component2
Expand All @@ -36,14 +38,21 @@ class DemoController(
private val demoRunnerFactory: RunnerFactory,
) {
/**
* @return all [DemoStatus.RUNNING] [DemoDto]s as [Flux]
* @param filter [DemoFilter], [DemoFilter.any] by default
* @param demoAmount number of [DemoDto]s that should be fetched, [DemoDto.DEFAULT_FETCH_NUMBER] by default
* @return all [DemoDto]s matching [filter] as [Flux]
*/
@GetMapping("/active")
fun active(): Flux<DemoDto> = blockingToFlux {
demoService.getAllDemos().map { it to demoService.getStatus(it).block() }
}
.filter { (_, status) -> status == DemoStatus.RUNNING }
.map { it.first.toDto() }
@PostMapping("/demo-list")
fun getFilteredDemoList(
@RequestBody(required = false) filter: DemoFilter?,
@RequestParam(required = false, defaultValue = DemoDto.DEFAULT_FETCH_NUMBER.toString())
demoAmount: Int = DemoDto.DEFAULT_FETCH_NUMBER,
): Flux<DemoDto> = filter.toMono()
.switchIfEmpty(DemoFilter.any.toMono())
.flatMapMany { demoFilter ->
blockingToFlux { demoService.getFiltered(demoFilter, demoAmount) }
}
.map { it.toDto() }

/**
* @param organizationName saveourtool organization name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import com.saveourtool.save.demo.repository.DemoRepository
import com.saveourtool.save.demo.repository.RunCommandRepository
import com.saveourtool.save.demo.runners.RunnerFactory
import com.saveourtool.save.demo.storage.DependencyStorage
import com.saveourtool.save.filters.DemoFilter
import com.saveourtool.save.utils.StringResponse
import com.saveourtool.save.utils.blockingToMono
import com.saveourtool.save.utils.switchIfEmptyToNotFound

import org.springframework.data.domain.PageRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import reactor.core.publisher.Mono
Expand Down Expand Up @@ -88,9 +90,31 @@ class DemoService(
)

/**
* @return list of [Demo]s that are stored in database
* @param demoFilter [DemoFilter]
* @param pageSize amount of [Demo]s that should be fetched
* @return list of [Demo]s that match [DemoFilter]
*/
fun getAllDemos(): List<Demo> = demoRepository.findAll()
fun getFiltered(demoFilter: DemoFilter, pageSize: Int): List<Demo> = demoRepository.findAll({ root, _, cb ->
with(demoFilter) {
val organizationNamePredicate = if (organizationName.isBlank()) {
cb.and()
} else {
cb.like(root.get("organizationName"), "%$organizationName%")
}
val projectNamePredicate = if (projectName.isBlank()) {
cb.and()
} else {
cb.like(root.get("projectName"), "%$projectName%")
}

cb.and(
organizationNamePredicate,
projectNamePredicate,
)
}
}, PageRequest.ofSize(pageSize))
.filter { demoFilter.statuses.isEmpty() || getStatus(it).block() in demoFilter.statuses }
.toList()

/**
* @param demo [Demo] entity
Expand Down Expand Up @@ -131,7 +155,7 @@ class DemoService(
* @param projectName saveourtool project name
* @return [Demo] connected with project [organizationName]/[projectName] or null if not present
*/
@Transactional
@Transactional(readOnly = true)
fun findBySaveourtoolProject(
organizationName: String,
projectName: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,44 @@
package com.saveourtool.save.frontend.components.basic.demo.welcome

import com.saveourtool.save.demo.DemoDto
import com.saveourtool.save.filters.DemoFilter
import com.saveourtool.save.frontend.components.basic.cardComponent
import com.saveourtool.save.frontend.utils.*
import com.saveourtool.save.frontend.utils.noopLoadingHandler
import com.saveourtool.save.frontend.utils.noopResponseHandler

import csstype.ClassName
import js.core.jso
import react.VFC
import react.dom.html.ReactHTML.div
import react.dom.html.ReactHTML.input
import react.dom.html.ReactHTML.ul
import react.router.dom.Link
import react.useEffect
import react.useState

private val withBackground = cardComponent(isBordered = true, hasBg = true)
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

val demoList = VFC {
val (organizationName, setOrganizationName) = useState("")
val (projectName, setProjectName) = useState("")
private val withBackground = cardComponent(isBordered = true, hasBg = true, isPaddingBottomNull = true)

val (demos, setDemos) = useState<List<DemoDto>>(emptyList())
val demoList = VFC {
val (filter, setFilter) = useState(DemoFilter.running)

useRequest {
val fetchedDemos: List<DemoDto> = get(
url = "$demoApiUrl/active",
val (demoDtos, setDemoDtos) = useState<List<DemoDto>>(emptyList())
val getFilteredDemos = useDebouncedDeferredRequest {
val demos: List<DemoDto> = post(
url = "$demoApiUrl/demo-list",
params = jso<dynamic> { demoAmount = DemoDto.DEFAULT_FETCH_NUMBER },
headers = jsonHeaders,
body = Json.encodeToString(filter),
loadingHandler = ::noopLoadingHandler,
responseHandler = ::noopResponseHandler
)
.unsafeMap { response ->
if (response.ok) {
response.decodeFromJsonString()
} else {
emptyList()
}
}
@Suppress("MAGIC_NUMBER")
setDemos(fetchedDemos.take(3))
.decodeFromJsonString()
setDemoDtos(demos)
}

useEffect(filter) { getFilteredDemos() }

withBackground {
div {
className = ClassName("m-5")
Expand All @@ -49,14 +51,29 @@ val demoList = VFC {
input {
className = ClassName("form-control")
placeholder = "Organization"
value = organizationName
onChange = { setOrganizationName(it.target.value) }
value = filter.organizationName
onChange = { event ->
setFilter { oldFilter -> oldFilter.copy(organizationName = event.target.value) }
}
}
input {
className = ClassName("form-control")
placeholder = "Project"
value = projectName
onChange = { setProjectName(it.target.value) }
value = filter.projectName
onChange = { event ->
setFilter { oldFilter -> oldFilter.copy(projectName = event.target.value) }
}
}
}
}

ul {
className = ClassName("list-group list-group-flush")
demoDtos.map { demoDto ->
Link {
to = "/demo/${demoDto.projectCoordinates}"
className = ClassName("list-group-item")
+demoDto.projectCoordinates.toString()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ package com.saveourtool.save.frontend.components.basic.testsuiteselector
import com.saveourtool.save.filters.TestSuiteFilter
import com.saveourtool.save.frontend.components.basic.showAvailableTestSuites
import com.saveourtool.save.frontend.components.basic.testsuiteselector.TestSuiteSelectorPurpose.CONTEST
import com.saveourtool.save.frontend.externals.lodash.debounce
import com.saveourtool.save.frontend.utils.*
import com.saveourtool.save.frontend.utils.noopResponseHandler
import com.saveourtool.save.testsuite.TestSuiteVersioned
Expand Down Expand Up @@ -88,24 +87,21 @@ private fun testSuiteSelectorSearchMode() = FC<TestSuiteSelectorSearchModeProps>
val (selectedTestSuites, setSelectedTestSuites) = useState(props.preselectedTestSuites)
val (filteredTestSuites, setFilteredTestSuites) = useState<List<TestSuiteVersioned>>(emptyList())
val (filters, setFilters) = useState(TestSuiteFilter.empty)
val getFilteredTestSuites = debounce(
useDeferredRequest {
if (filters.isNotEmpty()) {
val testSuitesFromBackend: List<TestSuiteVersioned> = get(
url = "$apiUrl/test-suites/${props.currentOrganizationName}/filtered${
filters.copy(language = encodeURIComponent(filters.language))
.toQueryParams("isContest" to "${props.selectorPurpose == CONTEST}")
}",
headers = jsonHeaders,
loadingHandler = ::noopLoadingHandler,
responseHandler = ::noopResponseHandler,
)
.decodeFromJsonString()
setFilteredTestSuites(testSuitesFromBackend)
}
},
DEFAULT_DEBOUNCE_PERIOD,
)
val getFilteredTestSuites = useDebouncedDeferredRequest(DEFAULT_DEBOUNCE_PERIOD) {
if (filters.isNotEmpty()) {
val testSuitesFromBackend: List<TestSuiteVersioned> = get(
url = "$apiUrl/test-suites/${props.currentOrganizationName}/filtered${
filters.copy(language = encodeURIComponent(filters.language))
.toQueryParams("isContest" to "${props.selectorPurpose == CONTEST}")
}",
headers = jsonHeaders,
loadingHandler = ::noopLoadingHandler,
responseHandler = ::noopResponseHandler,
)
.decodeFromJsonString()
setFilteredTestSuites(testSuitesFromBackend)
}
}

useEffect(filters) {
if (filters.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

package com.saveourtool.save.frontend.components.inputform

import com.saveourtool.save.frontend.externals.lodash.debounce
import com.saveourtool.save.frontend.utils.*
import com.saveourtool.save.frontend.utils.noopLoadingHandler
import com.saveourtool.save.frontend.utils.noopResponseHandler
Expand Down Expand Up @@ -84,25 +83,22 @@ external interface InputWithDebounceProps<T> : Props {

private fun <T> inputWithDebounce() = FC<InputWithDebounceProps<T>> { props ->
val (options, setOptions) = useState<List<T>>(emptyList())
val getOptions = debounce(
useDeferredRequest {
if (props.getString(props.selectedOption).isNotBlank()) {
val optionsFromBackend: List<T> = get(
url = props.getUrlForOptions(props.getString(props.selectedOption)),
headers = jsonHeaders,
loadingHandler = ::noopLoadingHandler,
responseHandler = ::noopResponseHandler,
)
.unsafeMap {
props.decodeListFromJsonString(it)
}
setOptions(optionsFromBackend)
} else {
setOptions(emptyList())
}
},
props.debouncePeriod ?: DEFAULT_DEBOUNCE_PERIOD,
)
val getOptions = useDebouncedDeferredRequest(props.debouncePeriod ?: DEFAULT_DEBOUNCE_PERIOD) {
if (props.getString(props.selectedOption).isNotBlank()) {
val optionsFromBackend: List<T> = get(
url = props.getUrlForOptions(props.getString(props.selectedOption)),
headers = jsonHeaders,
loadingHandler = ::noopLoadingHandler,
responseHandler = ::noopResponseHandler,
)
.unsafeMap {
props.decodeListFromJsonString(it)
}
setOptions(optionsFromBackend)
} else {
setOptions(emptyList())
}
}

useEffect(props.selectedOption) {
getOptions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,17 @@ val demoMainView: VFC = VFC {
div {
className = ClassName("page-header align-items-start min-vh-100")
div {
className = ClassName("row justify-content-center")

className = ClassName("d-flex justify-content-center")
div {
className = ClassName("col-md-4")
div {
className = ClassName("mb-2")
introductionSection()
}
className = ClassName("col-md-4 d-flex align-items-stretch")
div {
featuredDemos()
div {
className = ClassName("mb-2")
introductionSection()
}
div {
featuredDemos()
}
}
}
div {
Expand Down
Loading