Skip to content
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
69 changes: 38 additions & 31 deletions frontend/src/components/admin/tabs/stats-tab.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,11 @@
<template>
<div v-if="stats" id="stats">
<AdminKeyValueTable title="Platform" :items="platformTable" />
<!--
API Key + regenerate button moved to the Settings tab. The
stats payload still exposes ``stats.config.apiKey`` but the
Config table here drops it from the rendered keys.
-->
<AdminKeyValueTable title="Config" :items="configTable" />
<AdminKeyValueTable title="File Types" :items="fileTypesTable" />
<!-- Two denominators, named apart. Sessions counts settings rows, one per
user and per anonymous session; per user counts people, one vote
each. Titling either of them "User Settings" invited reading one as
the other. -->
<AdminKeyValueTable
title="Settings by Session"
:items="sessionSettingsTable"
v-for="section of sections"
:key="section.title"
:title="section.title"
:items="section.items"
/>
<AdminKeyValueTable
title="Settings by User"
:items="perUserSettingsTable"
/>
<AdminKeyValueTable
title="Browser Collections"
:items="browserCollectionsTable"
/>
<AdminKeyValueTable title="Tags" :items="metadataTable" />
<AdminKeyValueTable title="Reading" :items="usageTable" />
<AdminKeyValueTable title="Identifiers" :items="identifiersTable" />
<AdminKeyValueTable title="Admin Flags" :items="adminFlagsTable" />
<AdminKeyValueTable title="Online Tagging" :items="taggingTable" />
<AdminKeyValueTable title="Authentication" :items="authTable" />
<AdminKeyValueTable title="Email" :items="emailTable" />
<AdminKeyValueTable title="Rate Limits" :items="throttleTable" />
<AdminKeyValueTable title="Deployment" :items="deploymentTable" />
</div>
</template>

Expand Down Expand Up @@ -174,6 +147,34 @@ const INDENT_KEYS = Object.freeze(
const SET_SUFFIXES = ["Set", "Custom", "Configured", "Credentials"];
const NONE = "None";

// Every stats table, in render order: the caption it wears and the computed
// that fills it. Exported so a test enumerates the sections from here instead
// of keeping a second copy of the titles that goes stale when one is renamed.
const SECTIONS = Object.freeze([
["Platform", "platformTable"],
// The API key is deliberately absent: the payload still carries
// ``stats.config.apiKey``, but configTable drops it from the rendered keys.
// The key itself and its regenerate button live on the Settings tab.
["Config", "configTable"],
["File Types", "fileTypesTable"],
// Two denominators, named apart. Sessions counts settings rows, one per user
// and per anonymous session; per user counts people, one vote each. Titling
// either of them "User Settings" invited reading one as the other.
["Settings by Session", "sessionSettingsTable"],
["Settings by User", "perUserSettingsTable"],
["Browser Collections", "browserCollectionsTable"],
["Tags", "metadataTable"],
["Reading", "usageTable"],
["Identifiers", "identifiersTable"],
["Admin Flags", "adminFlagsTable"],
["Online Tagging", "taggingTable"],
["Authentication", "authTable"],
["Email", "emailTable"],
["Rate Limits", "throttleTable"],
["Deployment", "deploymentTable"],
]);
export const SECTION_TITLES = Object.freeze(SECTIONS.map(([title]) => title));

export default {
name: "AdminStatsTab",
components: {
Expand All @@ -185,6 +186,12 @@ export default {
};
},
computed: {
sections() {
return SECTIONS.map(([title, items]) => ({
title,
items: this[items],
}));
},
...mapState(useCommonStore, {}),
...mapState(useAdminStore, {
stats: (state) => state.stats,
Expand Down
36 changes: 13 additions & 23 deletions frontend/tests/unit/stats-tab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
*
* The tab is what an administrator sees of the anonymous stats report, so
* behavior locked in here:
* - Every section the API returns is rendered under a titled table.
* - Every section the component declares is rendered under a titled
* table. The titles come from the component so renaming one there
* does not leave a stale copy here.
* - Toggle booleans read as Yes/No, and "have you configured this"
* booleans read as Set/Not set, so nobody mistakes one for the other.
* - The API key is never rendered, even though the payload carries it.
Expand All @@ -12,7 +14,9 @@ import { createTestingPinia } from "@pinia/testing";
import { mount } from "@vue/test-utils";
import { describe, expect, test } from "vitest";

import StatsTab from "@/components/admin/tabs/stats-tab.vue";
import StatsTab, {
SECTION_TITLES,
} from "@/components/admin/tabs/stats-tab.vue";
import vuetify from "@/plugins/vuetify";

const STATS = {
Expand Down Expand Up @@ -104,23 +108,6 @@ const STATS = {
},
};

const SECTION_TITLES = [
"Platform",
"Config",
"File Types",
"User Settings",
"Browser Collections",
"Tags",
"Reading",
"Identifiers",
"Admin Flags",
"Online Tagging",
"Authentication",
"Email",
"Rate Limits",
"Deployment",
];

function mountTab(stats = STATS) {
const pinia = createTestingPinia({ initialState: { admin: { stats } } });
return mount(StatsTab, {
Expand All @@ -130,10 +117,13 @@ function mountTab(stats = STATS) {

describe("AdminStatsTab", () => {
test("renders a table for every stats section", () => {
const text = mountTab().text();
for (const title of SECTION_TITLES) {
expect(text).toContain(title);
}
const captions = mountTab()
.findAll(".adminKvCaption")
.map((caption) => caption.text());
// The whole ordered list, not per-title containment: a section that stops
// rendering, one rendered twice, and an empty title all have to fail here,
// which a loop of toContain over titles taken from the component cannot.
expect(captions).toStrictEqual([...SECTION_TITLES]);
});

test("never renders the api key", () => {
Expand Down