From c0a18bf874747bc20d56e28e5482aafdff2ac31e Mon Sep 17 00:00:00 2001 From: yamal Date: Fri, 18 Nov 2022 18:43:02 +0100 Subject: [PATCH 1/6] new omitFileBreakdown optional flag --- .../spotify/ruler/e2e/ReleaseReportTest.kt | 4 +- .../com/spotify/ruler/frontend/Utils.kt | 23 ++++ .../ruler/frontend/components/Breakdown.kt | 30 +++-- .../ruler/frontend/components/Insights.kt | 29 +++-- .../ruler/frontend/components/Ownership.kt | 113 +++++++++++++++--- .../spotify/ruler/plugin/RulerExtension.kt | 3 + .../com/spotify/ruler/plugin/RulerPlugin.kt | 2 + .../com/spotify/ruler/plugin/RulerTask.kt | 12 +- .../ruler/plugin/report/JsonReporter.kt | 52 ++++---- .../ruler/plugin/report/JsonReporterTest.kt | 20 +++- .../com/spotify/ruler/models/AppComponent.kt | 2 +- .../spotify/ruler/models/DynamicFeature.kt | 2 +- .../com/spotify/ruler/models/FileContainer.kt | 2 +- 13 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt diff --git a/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt b/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt index efa458c4..c953a679 100644 --- a/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt +++ b/ruler-e2e-tests/src/test/kotlin/com/spotify/ruler/e2e/ReleaseReportTest.kt @@ -97,7 +97,7 @@ class ReleaseReportTest { report.components.forEach { component -> var downloadSize = 0L var installSize = 0L - component.files.forEach { file -> + component.files?.forEach { file -> downloadSize += file.downloadSize installSize += file.installSize } @@ -111,7 +111,7 @@ class ReleaseReportTest { report.dynamicFeatures.forEach { feature -> var downloadSize = 0L var installSize = 0L - feature.files.forEach { file -> + feature.files?.forEach { file -> downloadSize += file.downloadSize installSize += file.installSize } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt new file mode 100644 index 00000000..a59e39eb --- /dev/null +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.spotify.ruler.frontend + +import com.spotify.ruler.models.FileContainer + +/** @return Given a list of FileContainers, it returns true if they don't contain any file breakdown. */ +fun List.filesWereOmitted(): Boolean = + all { it.files == null } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt index 4b315323..ce75ebcd 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt @@ -52,8 +52,10 @@ fun RBuilder.containerList(containers: List, sizeType: Measurable @Suppress("UNUSED_PARAMETER") fun RBuilder.containerListItem(id: Int, container: FileContainer, sizeType: Measurable.SizeType, @RKey key: String) { div(classes = "accordion-item") { - containerListItemHeader(id, container, sizeType) - containerListItemBody(id, container, sizeType) + container.files?.let { + containerListItemHeader(id, container, sizeType) + containerListItemBody(id, container, sizeType) + } ?: containerWithoutFilesListItemHeader(container, sizeType) } } @@ -63,21 +65,33 @@ fun RBuilder.containerListItemHeader(id: Int, container: FileContainer, sizeType button(classes = "accordion-button collapsed") { attrs["data-bs-toggle"] = "collapse" attrs["data-bs-target"] = "#module-$id-body" - span(classes = "font-monospace text-truncate me-3") { +container.name } - container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } - span(classes = "ms-auto me-3 text-nowrap") { - +formatSize(container, sizeType) - } + containerHeader(container, sizeType) } } } +@RFunction +fun RBuilder.containerWithoutFilesListItemHeader(container: FileContainer, sizeType: Measurable.SizeType) { + div(classes = "list-group-item d-flex border-0") { + containerHeader(container, sizeType) + } +} + +@RFunction +fun RBuilder.containerHeader(container: FileContainer, sizeType: Measurable.SizeType) { + span(classes = "font-monospace text-truncate me-3") { +container.name } + container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } + span(classes = "ms-auto me-3 text-nowrap") { + +formatSize(container, sizeType) + } +} + @RFunction fun RBuilder.containerListItemBody(id: Int, container: FileContainer, sizeType: Measurable.SizeType) { div(classes = "accordion-collapse collapse") { attrs.id = "module-$id-body" div(classes = "accordion-body p-0") { - fileList(container.files, sizeType) + fileList(container.files ?: emptyList(), sizeType) } } } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt index dd82e7f3..58fd3631 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt @@ -22,7 +22,9 @@ import com.spotify.ruler.frontend.chart.ChartConfig import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.frontend.chart.seriesOf +import com.spotify.ruler.frontend.filesWereOmitted import com.spotify.ruler.models.AppComponent +import com.spotify.ruler.models.AppFile import com.spotify.ruler.models.Measurable import kotlinx.browser.document import kotlinx.html.id @@ -34,25 +36,36 @@ import react.useEffect @RFunction fun RBuilder.insights(components: List) { - div(classes = "row mb-3") { - fileTypeGraphs(components) + val omitFileInsights = components.filesWereOmitted() + val componentFiles = if (omitFileInsights) { + emptyList() + } else { + components.flatMap { it.files ?: emptyList() } + } + + if (!omitFileInsights) { + div(classes = "row mb-3") { + fileTypeGraphs(componentFiles) + } } div(classes = "row") { componentTypeGraphs(components) } - div(classes = "row") { - resourcesTypeGraphs(components) + if (!omitFileInsights) { + div(classes = "row") { + resourcesTypeGraphs(componentFiles) + } } } @RFunction -fun RBuilder.fileTypeGraphs(components: List) { +fun RBuilder.fileTypeGraphs(files: List) { val labels = arrayOf("Classes", "Resources", "Assets", "Native libraries", "Other") val downloadSizes = LongArray(labels.size) val installSizes = LongArray(labels.size) val fileCounts = LongArray(labels.size) - components.flatMap(AppComponent::files).forEach { file -> + files.forEach { file -> val index = file.type.ordinal downloadSizes[index] += file.getSize(Measurable.SizeType.DOWNLOAD) installSizes[index] += file.getSize(Measurable.SizeType.INSTALL) @@ -122,13 +135,13 @@ fun RBuilder.componentTypeGraphs(components: List) { } @RFunction -fun RBuilder.resourcesTypeGraphs(components: List) { +fun RBuilder.resourcesTypeGraphs(files: List) { val labels = arrayOf("Drawable", "Layout", "Raw", "Values", "Font", "Other") val downloadSizes = LongArray(labels.size) val installSizes = LongArray(labels.size) val fileCounts = LongArray(labels.size) - components.flatMap(AppComponent::files).filter { it.resourceType != null }.forEach { file -> + files.filter { it.resourceType != null }.forEach { file -> val index = file.resourceType!!.ordinal downloadSizes[index] += file.getSize(Measurable.SizeType.DOWNLOAD) installSizes[index] += file.getSize(Measurable.SizeType.INSTALL) diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt index 5bab9a4a..f87df1d8 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt @@ -20,6 +20,7 @@ import com.bnorm.react.RFunction import com.spotify.ruler.frontend.binding.NumberFormatter import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.chart.seriesOf +import com.spotify.ruler.frontend.filesWereOmitted import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.models.AppComponent import com.spotify.ruler.models.AppFile @@ -36,19 +37,16 @@ const val PAGE_SIZE = 10 @RFunction fun RBuilder.ownership(components: List, sizeType: Measurable.SizeType) { componentOwnershipOverview(components) - componentOwnershipPerTeam(components, sizeType) + if (components.filesWereOmitted()) { + componentOwnershipPerTeamWithoutFilesBreakdown(components, sizeType) + } else { + componentOwnershipPerTeamWithFilesBreakdown(components, sizeType) + } } @RFunction fun RBuilder.componentOwnershipOverview(components: List) { - val sizes = mutableMapOf() - components.flatMap(AppComponent::files).forEach { file -> - val owner = file.owner ?: return@forEach - val current = sizes.getOrPut(owner) { Measurable.Mutable(0, 0) } - current.downloadSize += file.downloadSize - current.installSize += file.installSize - } - + val sizes = getSizesByOwner(components) val sorted = sizes.entries.sortedByDescending { (_, measurable) -> measurable.downloadSize } val owners = sorted.map { (owner, _) -> owner } val downloadSizes = sorted.map { (_, measurable) -> measurable.downloadSize } @@ -74,8 +72,33 @@ fun RBuilder.componentOwnershipOverview(components: List) { } @RFunction -fun RBuilder.componentOwnershipPerTeam(components: List, sizeType: Measurable.SizeType) { - val files = components.flatMap(AppComponent::files) +fun RBuilder.componentOwnershipPerTeamWithoutFilesBreakdown( + components: List, + sizeType: Measurable.SizeType, +) { + val owners = components.mapNotNull(AppComponent::owner).distinct().sorted() + var selectedOwner by useState(owners.first()) + + val ownedComponents = components.filter { component -> component.owner == selectedOwner } + + componentOwnershipPerTeam( + onSelectedOwnerUpdated = { owner -> selectedOwner = owner }, + owners = owners, + ownedComponentsCount = ownedComponents.size, + ownedFilesCount = null, + downloadSize = ownedComponents.sumOf(AppComponent::downloadSize), + installSize = ownedComponents.sumOf(AppComponent::installSize), + processedComponents = ownedComponents, + sizeType = sizeType, + ) +} + +@RFunction +fun RBuilder.componentOwnershipPerTeamWithFilesBreakdown( + components: List, + sizeType: Measurable.SizeType, +) { + val files = components.flatMap { it.files ?: emptyList() } val owners = files.mapNotNull(AppFile::owner).distinct().sorted() var selectedOwner by useState(owners.first()) @@ -84,8 +107,8 @@ fun RBuilder.componentOwnershipPerTeam(components: List, sizeType: val remainingOwnedFiles = ownedFiles.toMutableSet() val processedComponents = ownedComponents.map { component -> - val ownedFilesFromComponent = component.files.filter { file -> file.owner == selectedOwner } - remainingOwnedFiles.removeAll(ownedFilesFromComponent) + val ownedFilesFromComponent = component.files?.filter { file -> file.owner == selectedOwner } ?: emptyList() + remainingOwnedFiles.removeAll(ownedFilesFromComponent.toSet()) component.copy( downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), @@ -105,13 +128,36 @@ fun RBuilder.componentOwnershipPerTeam(components: List, sizeType: ) } + componentOwnershipPerTeam( + onSelectedOwnerUpdated = { owner -> selectedOwner = owner }, + owners = owners, + ownedComponentsCount = ownedComponents.size, + ownedFilesCount = ownedFiles.size, + downloadSize = ownedFiles.sumOf(AppFile::downloadSize), + installSize = ownedFiles.sumOf(AppFile::installSize), + processedComponents = processedComponents, + sizeType = sizeType, + ) +} + +@RFunction +fun RBuilder.componentOwnershipPerTeam( + onSelectedOwnerUpdated: (owner: String) -> Unit, + owners: List, + ownedComponentsCount: Int, + ownedFilesCount: Int?, + downloadSize: Long, + installSize: Long, + processedComponents: List, + sizeType: Measurable.SizeType, +) { h4(classes = "mb-3 mt-4") { +"Components and files grouped by owner" } - dropdown(owners, "owner-dropdown") { owner -> selectedOwner = owner } + dropdown(owners, "owner-dropdown", onSelectedOwnerUpdated) div(classes = "row mt-4 mb-4") { - highlightedValue(ownedComponents.size, "Component(s)") - highlightedValue(ownedFiles.size, "File(s)") - highlightedValue(ownedFiles.sumOf(AppFile::downloadSize), "Download size", ::formatSize) - highlightedValue(ownedFiles.sumOf(AppFile::installSize), "Install size", ::formatSize) + highlightedValue(ownedComponentsCount, "Component(s)") + ownedFilesCount?.let { highlightedValue(it, "File(s)") } + highlightedValue(downloadSize, "Download size", ::formatSize) + highlightedValue(installSize, "Install size", ::formatSize) } containerList(processedComponents, sizeType) } @@ -123,3 +169,34 @@ fun RBuilder.highlightedValue(value: Number, label: String, formatter: NumberFor span(classes = "text-muted m-0") { +label } } } + +private fun getSizesByOwner(components: List): Map { + val sizes = mutableMapOf() + + val omitFileOwnership = components.filesWereOmitted() + if (omitFileOwnership) { + sizes.populateWithSizesByOwner( + getOwner = { component -> component.owner }, + measurables = components, + ) + } else { + sizes.populateWithSizesByOwner( + getOwner = { file -> file.owner }, + measurables = components.flatMap { it.files ?: emptyList() }, + ) + } + + return sizes +} + +private inline fun MutableMap.populateWithSizesByOwner( + getOwner: (T) -> String?, + measurables: List, +) { + measurables.forEach { measurable -> + val owner = getOwner(measurable) ?: return@forEach + val current = getOrPut(owner) { Measurable.Mutable(0, 0) } + current.downloadSize += measurable.downloadSize + current.installSize += measurable.installSize + } +} diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt index 9b45f940..9d75f0db 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerExtension.kt @@ -29,8 +29,11 @@ open class RulerExtension(objects: ObjectFactory) { val ownershipFile: RegularFileProperty = objects.fileProperty() val defaultOwner: Property = objects.property(String::class.java) + val omitFileBreakdown: Property = objects.property(Boolean::class.java) + // Set up default values init { defaultOwner.convention("unknown") + omitFileBreakdown.convention(false) } } diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt index 05d7b27e..ff9f2f95 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerPlugin.kt @@ -53,6 +53,8 @@ class RulerPlugin : Plugin { task.workingDir.set(project.layout.buildDirectory.dir("intermediates/ruler/${variant.name}")) task.reportDir.set(project.layout.buildDirectory.dir("reports/ruler/${variant.name}")) + task.omitFileBreakdown.set(rulerExtension.omitFileBreakdown) + // Add explicit dependency to support DexGuard task.dependsOn("bundle$variantName") } diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt index 161565f2..bf60e82e 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/RulerTask.kt @@ -69,6 +69,9 @@ abstract class RulerTask : DefaultTask() { @get:Input abstract val defaultOwner: Property + @get:Input + abstract val omitFileBreakdown: Property + @get:OutputDirectory abstract val workingDir: DirectoryProperty @@ -132,7 +135,14 @@ abstract class RulerTask : DefaultTask() { val reportDir = reportDir.asFile.get() val jsonReporter = JsonReporter() - val jsonReport = jsonReporter.generateReport(appInfo.get(), components, features, ownershipInfo, reportDir) + val jsonReport = jsonReporter.generateReport( + appInfo.get(), + components, + features, + ownershipInfo, + reportDir, + omitFileBreakdown.get() + ) project.logger.lifecycle("Wrote JSON report to ${jsonReport.toPath().toUri()}") val htmlReporter = HtmlReporter() diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt index a487a6b5..4b78a9ce 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt @@ -39,6 +39,7 @@ class JsonReporter { * @param components Map of app component names to their respective files * @param ownershipInfo Optional info about the owners of components. * @param targetDir Directory where the generated report will be located + * @param omitFileBreakdown If true, the list of files for each component and dynamic feature will be omitted * @return Generated JSON report file */ fun generateReport( @@ -46,7 +47,8 @@ class JsonReporter { components: Map>, features: Map>, ownershipInfo: OwnershipInfo?, - targetDir: File + targetDir: File, + omitFileBreakdown: Boolean, ): File { val report = AppReport( name = appInfo.applicationId, @@ -61,16 +63,20 @@ class JsonReporter { downloadSize = files.sumOf(AppFile::downloadSize), installSize = files.sumOf(AppFile::installSize), owner = ownershipInfo?.getOwner(component.name, component.type), - files = files.map { file -> - AppFile( - name = file.name, - type = file.type, - downloadSize = file.downloadSize, - installSize = file.installSize, - owner = ownershipInfo?.getOwner(file.name, component.name, component.type), - resourceType = file.resourceType, - ) - }.sortedWith(comparator.reversed()) + files = if (omitFileBreakdown) { + null + } else { + files.map { file -> + AppFile( + name = file.name, + type = file.type, + downloadSize = file.downloadSize, + installSize = file.installSize, + owner = ownershipInfo?.getOwner(file.name, component.name, component.type), + resourceType = file.resourceType, + ) + }.sortedWith(comparator.reversed()) + } ) }.sortedWith(comparator.reversed()), dynamicFeatures = features.map { (feature, files) -> @@ -79,16 +85,20 @@ class JsonReporter { downloadSize = files.sumOf(AppFile::downloadSize), installSize = files.sumOf(AppFile::installSize), owner = ownershipInfo?.getOwner(feature), - files = files.map { file -> - AppFile( - name = file.name, - type = file.type, - downloadSize = file.downloadSize, - installSize = file.installSize, - owner = ownershipInfo?.getOwner(file.name, feature), - resourceType = file.resourceType, - ) - }.sortedWith(comparator.reversed()) + files = if (omitFileBreakdown) { + null + } else { + files.map { file -> + AppFile( + name = file.name, + type = file.type, + downloadSize = file.downloadSize, + installSize = file.installSize, + owner = ownershipInfo?.getOwner(file.name, feature), + resourceType = file.resourceType, + ) + }.sortedWith(comparator.reversed()) + } ) }.sortedWith(comparator.reversed()), ) diff --git a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt index 43d628e3..7728edac 100644 --- a/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt +++ b/ruler-gradle-plugin/src/test/kotlin/com/spotify/ruler/plugin/report/JsonReporterTest.kt @@ -65,7 +65,7 @@ class JsonReporterTest { @Test fun `JSON report is generated`(@TempDir targetDir: File) { - val reportFile = reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir) + val reportFile = reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir, false) val report = Json.decodeFromString(reportFile.readText()) val expected = AppReport("com.spotify.music", "1.2.3", "release", 750, 1050, listOf( @@ -92,9 +92,23 @@ class JsonReporterTest { assertThat(report).isEqualTo(expected) } + @Test + fun `JSON report is generated without file breakdown`(@TempDir targetDir: File) { + val reportFile = reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir, true) + val report = Json.decodeFromString(reportFile.readText()) + + val expected = AppReport("com.spotify.music", "1.2.3", "release", 750, 1050, listOf( + AppComponent(":lib", ComponentType.INTERNAL, 500, 600, null, "default-team"), + AppComponent(":app", ComponentType.INTERNAL, 250, 450, null, "app-team"), + ), listOf( + DynamicFeature("dynamic", 300, 550, null, "dynamic-team"), + )) + assertThat(report).isEqualTo(expected) + } + @Test fun `Ownership info is omitted from report if it is null`(@TempDir targetDir: File) { - val reportFile = reporter.generateReport(appInfo, components, features, null, targetDir) + val reportFile = reporter.generateReport(appInfo, components, features, null, targetDir, false) val reportContent = reportFile.readText() assertThat(reportContent).doesNotContain("owner") } @@ -102,7 +116,7 @@ class JsonReporterTest { @Test fun `Existing reports are overwritten`(@TempDir targetDir: File) { repeat(2) { - reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir) + reporter.generateReport(appInfo, components, features, ownershipInfo, targetDir, false) } } } diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt index 6feeb290..d5c7ec77 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/AppComponent.kt @@ -25,6 +25,6 @@ data class AppComponent( val type: ComponentType, override val downloadSize: Long, override val installSize: Long, - override val files: List, + override val files: List?, override val owner: String? = null, ) : FileContainer diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt index 4b27a730..85054c17 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/DynamicFeature.kt @@ -24,6 +24,6 @@ data class DynamicFeature( override val name: String, override val downloadSize: Long, override val installSize: Long, - override val files: List, + override val files: List?, override val owner: String? = null, ) : FileContainer diff --git a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt index 6b66876c..e3003385 100644 --- a/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt +++ b/ruler-models/src/commonMain/kotlin/com/spotify/ruler/models/FileContainer.kt @@ -20,5 +20,5 @@ package com.spotify.ruler.models interface FileContainer : Measurable { val name: String val owner: String? - val files: List + val files: List? } From af067d3cadb3c406b8c0e73de3aba3cb08d0d374 Mon Sep 17 00:00:00 2001 From: yamal Date: Sun, 20 Nov 2022 14:50:28 +0100 Subject: [PATCH 2/6] include omitFileBreakdown section in README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index b8137e5b..b623209b 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,16 @@ ruler { } ``` +#### Omitting file breakdown + +Sometimes, a lighter version of the report is enough to have an overview about app size. With `omitFileBreakdown` flag set to `true`, all file related info will be omitted (list of files and charts). The size of the generated report is therefore drastically reduced (this can be useful when dealing with low disk space on CI machines, for example). + +```kotlin +ruler { + omitFileBreakdown.set(true) // false by default +} +``` + ### Running the task Once this is done, `analyzeBundle` tasks will be added for each of your app variants. Running this task will build the app and generate a HTML report, which you can use to analyze your app size. It will also generate a JSON report, in case you want to further process the data. From 0ef648d65d96e93ff5d47c62f785690814b92353 Mon Sep 17 00:00:00 2001 From: yamal Date: Mon, 21 Nov 2022 17:42:41 +0100 Subject: [PATCH 3/6] Revert "include omitFileBreakdown section in README" This reverts commit af067d3cadb3c406b8c0e73de3aba3cb08d0d374. --- README.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/README.md b/README.md index b623209b..b8137e5b 100644 --- a/README.md +++ b/README.md @@ -60,16 +60,6 @@ ruler { } ``` -#### Omitting file breakdown - -Sometimes, a lighter version of the report is enough to have an overview about app size. With `omitFileBreakdown` flag set to `true`, all file related info will be omitted (list of files and charts). The size of the generated report is therefore drastically reduced (this can be useful when dealing with low disk space on CI machines, for example). - -```kotlin -ruler { - omitFileBreakdown.set(true) // false by default -} -``` - ### Running the task Once this is done, `analyzeBundle` tasks will be added for each of your app variants. Running this task will build the app and generate a HTML report, which you can use to analyze your app size. It will also generate a JSON report, in case you want to further process the data. From 22ab5c7c66a9705b473c660e6b63af62f217baeb Mon Sep 17 00:00:00 2001 From: yamal Date: Tue, 22 Nov 2022 10:12:46 +0100 Subject: [PATCH 4/6] refactor ownership per team and breakdown collapsing UI --- .../com/spotify/ruler/frontend/Utils.kt | 23 --- .../ruler/frontend/components/Breakdown.kt | 34 ++--- .../ruler/frontend/components/Common.kt | 5 +- .../ruler/frontend/components/Insights.kt | 18 +-- .../ruler/frontend/components/Ownership.kt | 142 +++++++----------- ruler-frontend/src/main/resources/style.css | 4 + 6 files changed, 76 insertions(+), 150 deletions(-) delete mode 100644 ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt deleted file mode 100644 index a59e39eb..00000000 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/Utils.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2021 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.spotify.ruler.frontend - -import com.spotify.ruler.models.FileContainer - -/** @return Given a list of FileContainers, it returns true if they don't contain any file breakdown. */ -fun List.filesWereOmitted(): Boolean = - all { it.files == null } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt index ce75ebcd..7e361697 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt @@ -52,40 +52,30 @@ fun RBuilder.containerList(containers: List, sizeType: Measurable @Suppress("UNUSED_PARAMETER") fun RBuilder.containerListItem(id: Int, container: FileContainer, sizeType: Measurable.SizeType, @RKey key: String) { div(classes = "accordion-item") { - container.files?.let { - containerListItemHeader(id, container, sizeType) - containerListItemBody(id, container, sizeType) - } ?: containerWithoutFilesListItemHeader(container, sizeType) + containerListItemHeader(id, container, sizeType) + containerListItemBody(id, container, sizeType) } } @RFunction fun RBuilder.containerListItemHeader(id: Int, container: FileContainer, sizeType: Measurable.SizeType) { h2(classes = "accordion-header") { - button(classes = "accordion-button collapsed") { + var classes = "accordion-button collapsed" + if (container.files == null) { + classes = "$classes disabled" + } + button(classes = classes) { attrs["data-bs-toggle"] = "collapse" attrs["data-bs-target"] = "#module-$id-body" - containerHeader(container, sizeType) + span(classes = "font-monospace text-truncate me-3") { +container.name } + container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } + span(classes = "ms-auto me-3 text-nowrap") { + +formatSize(container, sizeType) + } } } } -@RFunction -fun RBuilder.containerWithoutFilesListItemHeader(container: FileContainer, sizeType: Measurable.SizeType) { - div(classes = "list-group-item d-flex border-0") { - containerHeader(container, sizeType) - } -} - -@RFunction -fun RBuilder.containerHeader(container: FileContainer, sizeType: Measurable.SizeType) { - span(classes = "font-monospace text-truncate me-3") { +container.name } - container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } - span(classes = "ms-auto me-3 text-nowrap") { - +formatSize(container, sizeType) - } -} - @RFunction fun RBuilder.containerListItemBody(id: Int, container: FileContainer, sizeType: Measurable.SizeType) { div(classes = "accordion-collapse collapse") { diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt index 2659d173..c722b24f 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Common.kt @@ -46,11 +46,12 @@ fun RBuilder.report(report: AppReport) { val hasOwnershipInfo = report.components.any { component -> component.owner != null } val hasDynamicFeatures = report.dynamicFeatures.isNotEmpty() + val hasFileLevelInfo = report.components.any { it.files != null } val tabs = listOf( Tab("/", "Breakdown") { breakdown(report.components, sizeType) }, - Tab("/insights", "Insights") { insights(report.components) }, - Tab("/ownership", "Ownership", hasOwnershipInfo) { ownership(report.components, sizeType) }, + Tab("/insights", "Insights") { insights(report.components, hasFileLevelInfo) }, + Tab("/ownership", "Ownership", hasOwnershipInfo) { ownership(report.components, hasFileLevelInfo, sizeType) }, Tab("/dynamic", "Dynamic features", hasDynamicFeatures) { dynamicFeatures(report.dynamicFeatures, sizeType) }, ) diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt index 58fd3631..37d897f2 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt @@ -22,7 +22,6 @@ import com.spotify.ruler.frontend.chart.ChartConfig import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.frontend.chart.seriesOf -import com.spotify.ruler.frontend.filesWereOmitted import com.spotify.ruler.models.AppComponent import com.spotify.ruler.models.AppFile import com.spotify.ruler.models.Measurable @@ -35,23 +34,16 @@ import react.dom.p import react.useEffect @RFunction -fun RBuilder.insights(components: List) { - val omitFileInsights = components.filesWereOmitted() - val componentFiles = if (omitFileInsights) { - emptyList() - } else { - components.flatMap { it.files ?: emptyList() } +fun RBuilder.insights(components: List, hasFileLevelInfo: Boolean) { + div(classes = "row") { + componentTypeGraphs(components) } - if (!omitFileInsights) { + if (hasFileLevelInfo) { + val componentFiles = components.mapNotNull(AppComponent::files).flatten() div(classes = "row mb-3") { fileTypeGraphs(componentFiles) } - } - div(classes = "row") { - componentTypeGraphs(components) - } - if (!omitFileInsights) { div(classes = "row") { resourcesTypeGraphs(componentFiles) } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt index f87df1d8..4400c4e1 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt @@ -20,7 +20,6 @@ import com.bnorm.react.RFunction import com.spotify.ruler.frontend.binding.NumberFormatter import com.spotify.ruler.frontend.chart.BarChartConfig import com.spotify.ruler.frontend.chart.seriesOf -import com.spotify.ruler.frontend.filesWereOmitted import com.spotify.ruler.frontend.formatSize import com.spotify.ruler.models.AppComponent import com.spotify.ruler.models.AppFile @@ -35,13 +34,9 @@ import react.useState const val PAGE_SIZE = 10 @RFunction -fun RBuilder.ownership(components: List, sizeType: Measurable.SizeType) { +fun RBuilder.ownership(components: List, hasFileLevelInfo: Boolean, sizeType: Measurable.SizeType) { componentOwnershipOverview(components) - if (components.filesWereOmitted()) { - componentOwnershipPerTeamWithoutFilesBreakdown(components, sizeType) - } else { - componentOwnershipPerTeamWithFilesBreakdown(components, sizeType) - } + componentOwnershipPerTeam(components, hasFileLevelInfo, sizeType) } @RFunction @@ -72,52 +67,36 @@ fun RBuilder.componentOwnershipOverview(components: List) { } @RFunction -fun RBuilder.componentOwnershipPerTeamWithoutFilesBreakdown( - components: List, - sizeType: Measurable.SizeType, -) { - val owners = components.mapNotNull(AppComponent::owner).distinct().sorted() - var selectedOwner by useState(owners.first()) - - val ownedComponents = components.filter { component -> component.owner == selectedOwner } - - componentOwnershipPerTeam( - onSelectedOwnerUpdated = { owner -> selectedOwner = owner }, - owners = owners, - ownedComponentsCount = ownedComponents.size, - ownedFilesCount = null, - downloadSize = ownedComponents.sumOf(AppComponent::downloadSize), - installSize = ownedComponents.sumOf(AppComponent::installSize), - processedComponents = ownedComponents, - sizeType = sizeType, - ) -} - -@RFunction -fun RBuilder.componentOwnershipPerTeamWithFilesBreakdown( +fun RBuilder.componentOwnershipPerTeam( components: List, + hasFileLevelInfo: Boolean, sizeType: Measurable.SizeType, ) { - val files = components.flatMap { it.files ?: emptyList() } - val owners = files.mapNotNull(AppFile::owner).distinct().sorted() + val files: List? = if (hasFileLevelInfo) components.mapNotNull(AppComponent::files).flatten() else null + val fileLevelOwners = files?.mapNotNull(AppFile::owner) + val owners: List = (fileLevelOwners ?: components.mapNotNull(AppComponent::owner)).distinct().sorted() var selectedOwner by useState(owners.first()) val ownedComponents = components.filter { component -> component.owner == selectedOwner } - val ownedFiles = files.filter { file -> file.owner == selectedOwner } + val ownedFiles = files?.filter { file -> file.owner == selectedOwner } - val remainingOwnedFiles = ownedFiles.toMutableSet() + val remainingOwnedFiles = ownedFiles?.toMutableSet() val processedComponents = ownedComponents.map { component -> - val ownedFilesFromComponent = component.files?.filter { file -> file.owner == selectedOwner } ?: emptyList() - remainingOwnedFiles.removeAll(ownedFilesFromComponent.toSet()) - component.copy( - downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), - installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), - files = ownedFilesFromComponent, - ) + val ownedFilesFromComponent = component.files?.filter { file -> file.owner == selectedOwner } + if (ownedFilesFromComponent != null) { + remainingOwnedFiles?.removeAll(ownedFilesFromComponent.toSet()) + component.copy( + downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), + installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), + files = ownedFilesFromComponent, + ) + } else { + component + } }.toMutableList() // Group together all owned files which belong to components not owned by the currently selected owner - if (remainingOwnedFiles.isNotEmpty()) { + if (!remainingOwnedFiles.isNullOrEmpty()) { processedComponents += AppComponent( name = "Other owned files", type = ComponentType.INTERNAL, @@ -128,34 +107,23 @@ fun RBuilder.componentOwnershipPerTeamWithFilesBreakdown( ) } - componentOwnershipPerTeam( - onSelectedOwnerUpdated = { owner -> selectedOwner = owner }, - owners = owners, - ownedComponentsCount = ownedComponents.size, - ownedFilesCount = ownedFiles.size, - downloadSize = ownedFiles.sumOf(AppFile::downloadSize), - installSize = ownedFiles.sumOf(AppFile::installSize), - processedComponents = processedComponents, - sizeType = sizeType, - ) -} + val downloadSize: Long + val installSize: Long + if (ownedFiles == null) { + // If there is no file-level ownership info, use component-level ownership info + downloadSize = ownedComponents.sumOf(AppComponent::downloadSize) + installSize = ownedComponents.sumOf(AppComponent::installSize) + } else { + // Otherwise rely on file-level ownership info + downloadSize = ownedFiles.sumOf(AppFile::downloadSize) + installSize = ownedFiles.sumOf(AppFile::installSize) + } -@RFunction -fun RBuilder.componentOwnershipPerTeam( - onSelectedOwnerUpdated: (owner: String) -> Unit, - owners: List, - ownedComponentsCount: Int, - ownedFilesCount: Int?, - downloadSize: Long, - installSize: Long, - processedComponents: List, - sizeType: Measurable.SizeType, -) { h4(classes = "mb-3 mt-4") { +"Components and files grouped by owner" } - dropdown(owners, "owner-dropdown", onSelectedOwnerUpdated) + dropdown(owners, "owner-dropdown") { owner -> selectedOwner = owner } div(classes = "row mt-4 mb-4") { - highlightedValue(ownedComponentsCount, "Component(s)") - ownedFilesCount?.let { highlightedValue(it, "File(s)") } + highlightedValue(ownedComponents.size, "Component(s)") + ownedFiles?.size?.let { highlightedValue(it, "File(s)") } highlightedValue(downloadSize, "Download size", ::formatSize) highlightedValue(installSize, "Install size", ::formatSize) } @@ -173,30 +141,24 @@ fun RBuilder.highlightedValue(value: Number, label: String, formatter: NumberFor private fun getSizesByOwner(components: List): Map { val sizes = mutableMapOf() - val omitFileOwnership = components.filesWereOmitted() - if (omitFileOwnership) { - sizes.populateWithSizesByOwner( - getOwner = { component -> component.owner }, - measurables = components, - ) - } else { - sizes.populateWithSizesByOwner( - getOwner = { file -> file.owner }, - measurables = components.flatMap { it.files ?: emptyList() }, - ) + components.forEach { component -> + // If there is no file-level ownership info, use component-level ownership info + if (component.files == null) { + val owner = component.owner ?: return@forEach + val current = sizes.getOrPut(owner) { Measurable.Mutable(0, 0) } + current.downloadSize += component.downloadSize + current.installSize += component.installSize + return@forEach + } + + // Otherwise rely on file-level ownership info + component.files?.forEach fileLevelLoop@ { file -> + val owner = file.owner ?: return@fileLevelLoop + val current = sizes.getOrPut(owner) { Measurable.Mutable(0, 0) } + current.downloadSize += file.downloadSize + current.installSize += file.installSize + } } return sizes } - -private inline fun MutableMap.populateWithSizesByOwner( - getOwner: (T) -> String?, - measurables: List, -) { - measurables.forEach { measurable -> - val owner = getOwner(measurable) ?: return@forEach - val current = getOrPut(owner) { Measurable.Mutable(0, 0) } - current.downloadSize += measurable.downloadSize - current.installSize += measurable.installSize - } -} diff --git a/ruler-frontend/src/main/resources/style.css b/ruler-frontend/src/main/resources/style.css index e3c8d406..a92b670b 100644 --- a/ruler-frontend/src/main/resources/style.css +++ b/ruler-frontend/src/main/resources/style.css @@ -22,6 +22,10 @@ body { margin-left: unset !important; } +.accordion-button.disabled { + pointer-events: none; +} + .me-custom { margin-right: calc(24px + 1rem); } From 1c5c7840486fa964446d8521e8954be58e0ce9e5 Mon Sep 17 00:00:00 2001 From: yamal Date: Tue, 22 Nov 2022 17:08:21 +0100 Subject: [PATCH 5/6] fix Insights div class and improve code readability --- .../ruler/frontend/components/Insights.kt | 2 +- .../ruler/frontend/components/Ownership.kt | 34 +++++++++++-------- .../ruler/plugin/report/JsonReporter.kt | 1 + 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt index 37d897f2..10655151 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Insights.kt @@ -35,7 +35,7 @@ import react.useEffect @RFunction fun RBuilder.insights(components: List, hasFileLevelInfo: Boolean) { - div(classes = "row") { + div(classes = "row mb-3") { componentTypeGraphs(components) } diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt index 4400c4e1..fc309aeb 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Ownership.kt @@ -72,9 +72,16 @@ fun RBuilder.componentOwnershipPerTeam( hasFileLevelInfo: Boolean, sizeType: Measurable.SizeType, ) { - val files: List? = if (hasFileLevelInfo) components.mapNotNull(AppComponent::files).flatten() else null - val fileLevelOwners = files?.mapNotNull(AppFile::owner) - val owners: List = (fileLevelOwners ?: components.mapNotNull(AppComponent::owner)).distinct().sorted() + val files: List? + var owners: List + if (hasFileLevelInfo) { + files = components.mapNotNull(AppComponent::files).flatten() + owners = files.mapNotNull(AppFile::owner) + } else { + files = null + owners = components.mapNotNull(AppComponent::owner) + } + owners = owners.distinct().sorted() var selectedOwner by useState(owners.first()) val ownedComponents = components.filter { component -> component.owner == selectedOwner } @@ -82,17 +89,16 @@ fun RBuilder.componentOwnershipPerTeam( val remainingOwnedFiles = ownedFiles?.toMutableSet() val processedComponents = ownedComponents.map { component -> - val ownedFilesFromComponent = component.files?.filter { file -> file.owner == selectedOwner } - if (ownedFilesFromComponent != null) { - remainingOwnedFiles?.removeAll(ownedFilesFromComponent.toSet()) - component.copy( - downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), - installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), - files = ownedFilesFromComponent, - ) - } else { - component - } + val ownedFilesFromComponent = component.files?.filter { file -> + file.owner == selectedOwner + } ?: return@map component + + remainingOwnedFiles?.removeAll(ownedFilesFromComponent.toSet()) + component.copy( + downloadSize = ownedFilesFromComponent.sumOf(AppFile::downloadSize), + installSize = ownedFilesFromComponent.sumOf(AppFile::installSize), + files = ownedFilesFromComponent, + ) }.toMutableList() // Group together all owned files which belong to components not owned by the currently selected owner diff --git a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt index 4b78a9ce..737e44a3 100644 --- a/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt +++ b/ruler-gradle-plugin/src/main/kotlin/com/spotify/ruler/plugin/report/JsonReporter.kt @@ -42,6 +42,7 @@ class JsonReporter { * @param omitFileBreakdown If true, the list of files for each component and dynamic feature will be omitted * @return Generated JSON report file */ + @Suppress("LongParameterList") fun generateReport( appInfo: AppInfo, components: Map>, From 001a451ada5c6e099cb155e7efb0268fa8f9ed0b Mon Sep 17 00:00:00 2001 From: yamal Date: Tue, 29 Nov 2022 18:32:51 +0100 Subject: [PATCH 6/6] hide collapsing arrow when there are no files --- .../com/spotify/ruler/frontend/components/Breakdown.kt | 9 +++++++-- ruler-frontend/src/main/resources/style.css | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt index 7e361697..3c72af7d 100644 --- a/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt +++ b/ruler-frontend/src/main/kotlin/com/spotify/ruler/frontend/components/Breakdown.kt @@ -59,9 +59,10 @@ fun RBuilder.containerListItem(id: Int, container: FileContainer, sizeType: Meas @RFunction fun RBuilder.containerListItemHeader(id: Int, container: FileContainer, sizeType: Measurable.SizeType) { + val containsFiles = container.files != null h2(classes = "accordion-header") { var classes = "accordion-button collapsed" - if (container.files == null) { + if (!containsFiles) { classes = "$classes disabled" } button(classes = classes) { @@ -69,7 +70,11 @@ fun RBuilder.containerListItemHeader(id: Int, container: FileContainer, sizeType attrs["data-bs-target"] = "#module-$id-body" span(classes = "font-monospace text-truncate me-3") { +container.name } container.owner?.let { owner -> span(classes = "badge bg-secondary me-3") { +owner } } - span(classes = "ms-auto me-3 text-nowrap") { + var sizeClasses = "ms-auto text-nowrap" + if (containsFiles) { + sizeClasses = "$sizeClasses me-3" + } + span(classes = sizeClasses) { +formatSize(container, sizeType) } } diff --git a/ruler-frontend/src/main/resources/style.css b/ruler-frontend/src/main/resources/style.css index a92b670b..671585c8 100644 --- a/ruler-frontend/src/main/resources/style.css +++ b/ruler-frontend/src/main/resources/style.css @@ -26,6 +26,10 @@ body { pointer-events: none; } +.accordion-button.disabled::after { + display: none; +} + .me-custom { margin-right: calc(24px + 1rem); }