Skip to content

Commit

Permalink
feat: Add project dashboard page #915 - Dashboard with chart WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
JanCizmar committed Apr 22, 2022
1 parent dad3f88 commit 64f4d0b
Show file tree
Hide file tree
Showing 34 changed files with 1,257 additions and 111 deletions.
@@ -0,0 +1,100 @@
/*
* Copyright (c) 2020. Tolgee
*/

package io.tolgee.api.v2.controllers

import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import io.tolgee.api.v2.hateoas.project.stats.LanguageStatsModel
import io.tolgee.api.v2.hateoas.project.stats.ProjectStatsModel
import io.tolgee.constants.Message
import io.tolgee.exceptions.NotFoundException
import io.tolgee.model.views.projectStats.ProjectLanguageStatsResultView
import io.tolgee.security.api_key_auth.AccessWithApiKey
import io.tolgee.security.project_auth.AccessWithAnyProjectPermission
import io.tolgee.security.project_auth.ProjectHolder
import io.tolgee.service.ProjectService
import io.tolgee.service.ProjectStatsService
import org.springframework.hateoas.MediaTypes
import org.springframework.web.bind.annotation.CrossOrigin
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDate

@Suppress("MVCPathVariableInspection")
@RestController
@CrossOrigin(origins = ["*"])
@RequestMapping(value = ["/v2/projects/{projectId}/stats", "/v2/projects/stats"])
@Tag(name = "Projects")
class ProjectStatsController(
private val projectStatsService: ProjectStatsService,
private val projectHolder: ProjectHolder,
private val projectService: ProjectService
) {
@Operation(summary = "Returns project stats")
@GetMapping("", produces = [MediaTypes.HAL_JSON_VALUE])
@AccessWithAnyProjectPermission
@AccessWithApiKey
fun getProjectStats(@PathVariable projectId: Long): ProjectStatsModel {
val projectStats = projectStatsService.getProjectStats(projectId)
val languageStats = projectStatsService.getLanguageStats(projectId)

val baseLanguage = projectService.getOrCreateBaseLanguage(projectHolder.project.id)
val baseStats = languageStats.find { it.languageId == baseLanguage?.id }
?: throw NotFoundException(Message.BASE_LANGUAGE_NOT_FOUND)

val nonBaseLanguages = languageStats.filter { it.languageId != baseStats.languageId }
val baseWordsCount = baseStats.translatedWords + baseStats.reviewedWords

val allNonBaseTotalBaseWords = baseWordsCount * nonBaseLanguages.size
val allNonBaseTotalTranslatedWords = nonBaseLanguages.sumOf { it.translatedWords }
val allNonBaseTotalReviewedWords = nonBaseLanguages.sumOf { it.reviewedWords }

val translatedPercent = (allNonBaseTotalTranslatedWords.toDouble() / allNonBaseTotalBaseWords) * 100
val reviewedPercent = (allNonBaseTotalReviewedWords.toDouble() / allNonBaseTotalBaseWords) * 100

return ProjectStatsModel(
projectId = projectStats.id,
languageCount = languageStats.size,
keyCount = projectStats.keyCount,
baseWordsCount = baseWordsCount,
translatedPercentage = translatedPercent,
reviewedPercentage = reviewedPercent,
membersCount = projectStats.memberCount,
tagCount = projectStats.tagCount,
languageStats = getSortedLanguageStatModels(languageStats, baseStats)
)
}

@Operation(summary = "Returns project daily amount of events")
@GetMapping("/daily-activity", produces = [MediaTypes.HAL_JSON_VALUE])
@AccessWithAnyProjectPermission
@AccessWithApiKey
fun getProjectDailyActivity(@PathVariable projectId: Long): Map<LocalDate, Long> {
return projectStatsService.getProjectDailyActivity(projectId)
}

private fun getSortedLanguageStatModels(
languageStats: List<ProjectLanguageStatsResultView>,
baseStats: ProjectLanguageStatsResultView
) = languageStats.sortedBy { it.languageName }.sortedBy { it.languageId != baseStats.languageId }.map {
LanguageStatsModel(
languageId = it.languageId,
languageTag = it.languageTag,
languageName = it.languageName,
languageOriginalName = it.languageOriginalName,
languageFlagEmoji = it.languageFlagEmoji,
translatedKeyCount = it.translatedKeys,
translatedWordCount = it.translatedWords,
translatedPercentage = it.translatedWords.toDouble() /
(baseStats.translatedWords + baseStats.reviewedWords) * 100,
reviewedKeyCount = it.reviewedKeys,
reviewedWordCount = it.reviewedWords,
reviewedPercentage = it.reviewedWords.toDouble() /
(baseStats.translatedWords + baseStats.reviewedWords) * 100,
)
}
}

This file was deleted.

@@ -0,0 +1,25 @@
package io.tolgee.api.v2.hateoas.organization

import io.swagger.v3.oas.annotations.media.Schema
import io.tolgee.dtos.Avatar
import io.tolgee.model.Permission
import org.springframework.hateoas.RepresentationModel
import org.springframework.hateoas.server.core.Relation

@Relation(collectionRelation = "organizations", itemRelation = "organization")
open class SimpleOrganizationModel(
val id: Long,

@Schema(example = "Beautiful organization")
val name: String,

@Schema(example = "btforg")
val slug: String,

@Schema(example = "This is a beautiful organization full of beautiful and clever people")
val description: String?,
val basePermissions: Permission.ProjectPermissionType,

@Schema(example = "Links to avatar images")
var avatar: Avatar?
) : RepresentationModel<SimpleOrganizationModel>()
@@ -0,0 +1,27 @@
package io.tolgee.api.v2.hateoas.organization

import io.tolgee.api.v2.controllers.OrganizationController
import io.tolgee.model.Organization
import io.tolgee.service.AvatarService
import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport
import org.springframework.hateoas.server.mvc.linkTo
import org.springframework.stereotype.Component

@Component
class SimpleOrganizationModelAssembler(
private val avatarService: AvatarService
) : RepresentationModelAssemblerSupport<Organization, SimpleOrganizationModel>(
OrganizationController::class.java, SimpleOrganizationModel::class.java
) {
override fun toModel(entity: Organization): SimpleOrganizationModel {
val link = linkTo<OrganizationController> { get(entity.slug ?: "") }.withSelfRel()
return SimpleOrganizationModel(
entity.id,
entity.name!!,
entity.slug!!,
entity.description,
entity.basePermissions,
avatarService.getAvatarLinks(entity.avatarHash)
).add(link)
}
}
Expand Up @@ -3,6 +3,7 @@ package io.tolgee.api.v2.hateoas.project
import io.swagger.v3.oas.annotations.media.Schema
import io.tolgee.api.v2.hateoas.UserPermissionModel
import io.tolgee.api.v2.hateoas.language.LanguageModel
import io.tolgee.api.v2.hateoas.organization.SimpleOrganizationModel
import io.tolgee.api.v2.hateoas.user_account.UserAccountModel
import io.tolgee.dtos.Avatar
import io.tolgee.model.Permission
Expand All @@ -19,9 +20,13 @@ open class ProjectModel(
val slug: String?,
val avatar: Avatar?,
val userOwner: UserAccountModel?,
val organizationOwner: SimpleOrganizationModel?,
val baseLanguage: LanguageModel?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerName: String?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerSlug: String?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerBasePermissions: Permission.ProjectPermissionType?,
val organizationRole: OrganizationRoleType?,
@Schema(description = "Current user's direct permission", example = "MANAGE")
Expand Down
Expand Up @@ -4,6 +4,7 @@ import io.tolgee.api.v2.controllers.OrganizationController
import io.tolgee.api.v2.controllers.V2ProjectsController
import io.tolgee.api.v2.hateoas.UserPermissionModel
import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler
import io.tolgee.api.v2.hateoas.organization.SimpleOrganizationModelAssembler
import io.tolgee.api.v2.hateoas.user_account.UserAccountModelAssembler
import io.tolgee.model.views.ProjectWithLanguagesView
import io.tolgee.service.AvatarService
Expand All @@ -19,7 +20,8 @@ class ProjectModelAssembler(
private val permissionService: PermissionService,
private val projectService: ProjectService,
private val languageModelAssembler: LanguageModelAssembler,
private val avatarService: AvatarService
private val avatarService: AvatarService,
private val simpleOrganizationModelAssembler: SimpleOrganizationModelAssembler
) : RepresentationModelAssemblerSupport<ProjectWithLanguagesView, ProjectModel>(
V2ProjectsController::class.java, ProjectModel::class.java
) {
Expand All @@ -34,21 +36,22 @@ class ProjectModelAssembler(
description = view.description,
slug = view.slug,
avatar = avatarService.getAvatarLinks(view.avatarHash),
organizationOwnerSlug = view.organizationOwnerSlug,
organizationOwnerName = view.organizationOwnerName,
organizationOwnerBasePermissions = view.organizationBasePermissions,
organizationOwnerSlug = view.organizationOwner?.slug,
organizationOwnerName = view.organizationOwner?.name,
organizationOwnerBasePermissions = view.organizationOwner?.basePermissions,
organizationRole = view.organizationRole,
organizationOwner = view.organizationOwner?.let { simpleOrganizationModelAssembler.toModel(it) },
baseLanguage = baseLanguage?.let { languageModelAssembler.toModel(baseLanguage) },
userOwner = view.userOwner?.let { userAccountModelAssembler.toModel(it) },
directPermissions = view.directPermissions,
computedPermissions = UserPermissionModel(
type = permissionService.computeProjectPermissionType(
view.organizationRole, view.organizationBasePermissions, view.directPermissions, null
view.organizationRole, view.organizationOwner?.basePermissions, view.directPermissions, null
).type!!,
permittedLanguageIds = view.permittedLanguageIds
)
).add(link).also { model ->
view.organizationOwnerSlug?.let {
view.organizationOwner?.slug?.let {
model.add(linkTo<OrganizationController> { get(it) }.withRel("organizationOwner"))
}
}
Expand Down

This file was deleted.

Expand Up @@ -3,6 +3,7 @@ package io.tolgee.api.v2.hateoas.project
import io.swagger.v3.oas.annotations.media.Schema
import io.tolgee.api.v2.hateoas.UserPermissionModel
import io.tolgee.api.v2.hateoas.language.LanguageModel
import io.tolgee.api.v2.hateoas.organization.SimpleOrganizationModel
import io.tolgee.api.v2.hateoas.user_account.UserAccountModel
import io.tolgee.dtos.Avatar
import io.tolgee.dtos.query_results.ProjectStatistics
Expand All @@ -20,9 +21,13 @@ open class ProjectWithStatsModel(
val slug: String?,
val avatar: Avatar?,
val userOwner: UserAccountModel?,
val organizationOwner: SimpleOrganizationModel?,
val baseLanguage: LanguageModel?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerName: String?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerSlug: String?,
@Schema(deprecated = true, description = "Use organizationOwner field")
val organizationOwnerBasePermissions: Permission.ProjectPermissionType?,
val organizationRole: OrganizationRoleType?,
@Schema(description = "Current user's direct permission", example = "MANAGE")
Expand Down
Expand Up @@ -4,6 +4,7 @@ import io.tolgee.api.v2.controllers.OrganizationController
import io.tolgee.api.v2.controllers.V2ProjectsController
import io.tolgee.api.v2.hateoas.UserPermissionModel
import io.tolgee.api.v2.hateoas.language.LanguageModelAssembler
import io.tolgee.api.v2.hateoas.organization.SimpleOrganizationModelAssembler
import io.tolgee.api.v2.hateoas.user_account.UserAccountModelAssembler
import io.tolgee.model.views.ProjectWithStatsView
import io.tolgee.service.AvatarService
Expand All @@ -19,7 +20,8 @@ class ProjectWithStatsModelAssembler(
private val permissionService: PermissionService,
private val projectService: ProjectService,
private val languageModelAssembler: LanguageModelAssembler,
private val avatarService: AvatarService
private val avatarService: AvatarService,
private val simpleOrganizationModelAssembler: SimpleOrganizationModelAssembler
) : RepresentationModelAssemblerSupport<ProjectWithStatsView, ProjectWithStatsModel>(
V2ProjectsController::class.java, ProjectWithStatsModel::class.java
) {
Expand All @@ -34,23 +36,24 @@ class ProjectWithStatsModelAssembler(
description = view.description,
slug = view.slug,
avatar = avatarService.getAvatarLinks(view.avatarHash),
organizationOwnerSlug = view.organizationOwnerSlug,
organizationOwnerName = view.organizationOwnerName,
organizationOwnerBasePermissions = view.organizationBasePermissions,
organizationOwnerSlug = view.organizationOwner?.slug,
organizationOwnerName = view.organizationOwner?.name,
organizationOwnerBasePermissions = view.organizationOwner?.basePermissions,
organizationRole = view.organizationRole,
baseLanguage = baseLanguage?.let { languageModelAssembler.toModel(baseLanguage) },
userOwner = view.userOwner?.let { userAccountModelAssembler.toModel(it) },
organizationOwner = view.organizationOwner?.let { simpleOrganizationModelAssembler.toModel(it) },
directPermissions = view.directPermissions,
computedPermissions = UserPermissionModel(
type = permissionService.computeProjectPermissionType(
view.organizationRole, view.organizationBasePermissions, view.directPermissions, null
view.organizationRole, view.organizationOwner?.basePermissions, view.directPermissions, null
).type!!,
permittedLanguageIds = view.permittedLanguageIds
),
stats = view.stats,
languages = view.languages.map { languageModelAssembler.toModel(it) },
).add(link).also { model ->
view.organizationOwnerSlug?.let {
view.organizationOwner?.slug?.let {
model.add(linkTo<OrganizationController> { get(it) }.withRel("organizationOwner"))
}
}
Expand Down
@@ -0,0 +1,15 @@
package io.tolgee.api.v2.hateoas.project.stats

open class LanguageStatsModel(
val languageId: Long?,
val languageTag: String?,
val languageName: String?,
val languageOriginalName: String?,
val languageFlagEmoji: String?,
val translatedKeyCount: Long,
val translatedWordCount: Long,
val translatedPercentage: Double,
val reviewedKeyCount: Long,
val reviewedWordCount: Long,
val reviewedPercentage: Double,
)
@@ -0,0 +1,14 @@
package io.tolgee.api.v2.hateoas.project.stats

@Suppress("unused")
open class ProjectStatsModel(
val projectId: Long,
val languageCount: Int,
val keyCount: Long,
val baseWordsCount: Long,
val translatedPercentage: Double,
val reviewedPercentage: Double,
val membersCount: Long,
val tagCount: Long,
val languageStats: List<LanguageStatsModel>
)
Expand Up @@ -81,8 +81,8 @@ class ProjectRepositoryTest {
dbPopulatorReal.createBase("No org repo", users[3].username)
val result = projectRepository.findAllPermitted(users[3].id, PageRequest.of(0, 20, Sort.by(Sort.Order.asc("id"))))
assertThat(result).hasSize(10)
assertThat(result.content[0].organizationOwnerName).isNotNull
assertThat(result.content[8].organizationOwnerSlug).isNotNull
assertThat(result.content[0].organizationOwner?.name).isNotNull
assertThat(result.content[8].organizationOwner?.slug).isNotNull
assertThat(result.content[9].userOwner).isNotNull
assertThat(result.content[9].directPermissions).isNotNull
}
Expand Down
Expand Up @@ -25,7 +25,7 @@ internal class ProjectStatsServiceTest : AbstractSpringTest() {
testDataService.saveTestData(testData.root)
val data = projectStatsService.getProjectStats(testData.projectBuilder.self.id)
assertThat(data.id).isPositive
assertThat(data.userCount).isEqualTo(3)
assertThat(data.memberCount).isEqualTo(3)
assertThat(data.keyCount).isEqualTo(5)
assertThat(data.tagCount).isEqualTo(3)
}
Expand Down

0 comments on commit 64f4d0b

Please sign in to comment.