UX: Apply admin UI guidelines to Reports pages (#30684)

Applies the admin UI guidelines from
https://meta.discourse.org/t/creating-consistent-admin-interfaces/326780
to the reports list and single report page for admins.


![image](https://github.com/user-attachments/assets/2431901f-0225-4658-b408-ab0865d022e6)

![image](https://github.com/user-attachments/assets/6f9e531b-8fec-405f-8429-151fd261ee2c)

---------

Co-authored-by: Ella <ella.estigoy@gmail.com>
This commit is contained in:
Martin Brennan 2025-01-14 13:22:08 +10:00 committed by GitHub
parent 65dec020b8
commit dc0bf90069
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 148 additions and 132 deletions

View File

@ -4,51 +4,13 @@
{{#if this.showHeader}}
<div class="header">
{{#if this.showTitle}}
<ul class="breadcrumb">
{{#if this.showAllReportsLink}}
<li class="item all-reports">
<LinkTo @route="adminReports.index" class="report-url">
{{i18n "admin.dashboard.all_reports"}}
</LinkTo>
</li>
{{#unless this.showNotFoundError}}
<li class="item separator">|</li>
{{/unless}}
{{/if}}
{{#unless this.showNotFoundError}}
<li class="item report">
<a href={{this.model.reportUrl}} class="report-url">
{{this.model.title}}
</a>
{{#if this.model.description}}
<DTooltip
@interactive={{this.model.description_link.length}}
>
<:trigger>
{{d-icon "circle-question"}}
</:trigger>
<:content>
{{#if this.model.description_link}}
<a
target="_blank"
rel="noopener noreferrer"
href={{this.model.description_link}}
class="info"
>
{{this.model.description}}
</a>
{{else}}
<span>{{this.model.description}}</span>
{{/if}}
</:content>
</DTooltip>
{{/if}}
</li>
{{/unless}}
</ul>
{{#unless this.showNotFoundError}}
<DPageSubheader
@titleLabel={{this.model.title}}
@descriptionLabel={{this.model.description}}
@learnMoreUrl={{this.model.description_link}}
/>
{{/unless}}
{{/if}}
{{#if this.shouldDisplayTrend}}

View File

@ -40,7 +40,6 @@ export default class AdminReport extends Component {
model = null;
reportOptions = null;
forcedModes = null;
showAllReportsLink = false;
filters = null;
showTrend = false;
showHeader = true;

View File

@ -1,25 +1,29 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { Input } from "@ember/component";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { LinkTo } from "@ember/routing";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
import dIcon from "discourse/helpers/d-icon";
import withEventValue from "discourse/helpers/with-event-value";
import { ajax } from "discourse/lib/ajax";
import { bind } from "discourse/lib/decorators";
import { i18n } from "discourse-i18n";
import AdminSectionLandingItem from "admin/components/admin-section-landing-item";
import AdminSectionLandingWrapper from "admin/components/admin-section-landing-wrapper";
export default class AdminReports extends Component {
@service siteSettings;
@tracked reports = null;
@tracked filter = "";
@tracked isLoading = false;
@tracked isLoading = true;
constructor() {
super(...arguments);
this.loadReports();
}
@bind
loadReports() {
this.isLoading = true;
ajax("/admin/reports")
.then((json) => {
this.reports = json.reports;
@ -54,39 +58,28 @@ export default class AdminReports extends Component {
}
<template>
<div {{didInsert this.loadReports}}>
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
<div class="admin-reports-header">
<h2>{{i18n "admin.reports.title"}}</h2>
<Input
class="admin-reports-header__filter"
<ConditionalLoadingSpinner @condition={{this.isLoading}}>
<div class="d-admin-filter admin-reports-header">
<div class="admin-filter__input-container">
<input
type="text"
class="admin-filter__input admin-reports-header__filter"
placeholder={{i18n "admin.filter_reports"}}
@value={{this.filter}}
value={{this.filter}}
{{on "input" (withEventValue (fn (mut this.filter)))}}
/>
</div>
<div class="alert alert-info">
{{dIcon "book"}}
{{htmlSafe (i18n "admin.reports.meta_doc")}}
</div>
<ul class="admin-reports-list">
{{#each this.filteredReports as |report|}}
<li class="admin-reports-list__report">
<LinkTo @route="adminReports.show" @model={{report.type}}>
<h3
class="admin-reports-list__report-title"
>{{report.title}}</h3>
{{#if report.description}}
<p class="admin-reports-list__report-description">
{{report.description}}
</p>
{{/if}}
</LinkTo>
</li>
{{/each}}
</ul>
</ConditionalLoadingSpinner>
</div>
</div>
<AdminSectionLandingWrapper class="admin-reports-list">
{{#each this.filteredReports as |report|}}
<AdminSectionLandingItem
@titleLabelTranslated={{report.title}}
@descriptionLabelTranslated={{report.description}}
@titleRoute="adminReports.show"
@titleRouteModel={{report.type}}
/>
{{/each}}
</AdminSectionLandingWrapper>
</ConditionalLoadingSpinner>
</template>
}

View File

@ -54,9 +54,16 @@ export default class AdminSectionLandingItem extends Component {
{{/if}}
{{#if @titleRoute}}
<LinkTo @route={{@titleRoute}}><h3
class="admin-section-landing-item__title"
>{{this.title}}</h3></LinkTo>
{{#if @titleRouteModel}}
<LinkTo @route={{@titleRoute}} @model={{@titleRouteModel}}><h3
class="admin-section-landing-item__title"
>{{this.title}}</h3></LinkTo>
{{else}}
<LinkTo @route={{@titleRoute}}><h3
class="admin-section-landing-item__title"
>{{this.title}}</h3></LinkTo>
{{/if}}
{{else}}
<h3 class="admin-section-landing-item__title">{{this.title}}</h3>
{{/if}}

View File

@ -1 +1,20 @@
<AdminReports />
<DPageHeader
@titleLabel={{i18n "admin.reports.title"}}
@descriptionLabel={{i18n "admin.reports.meta_doc"}}
@learnMoreUrl="https://meta.discourse.org/t/-/240233"
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/reports"
@label={{i18n "admin.reports.sidebar_title"}}
/>
</:breadcrumbs>
</DPageHeader>
<div class="admin-container admin-config-page__main-area">
<div class="admin-config-area__full-width">
<AdminReports />
</div>
</div>

View File

@ -1,8 +1,12 @@
<AdminReport
@showAllReportsLink={{true}}
@dataSourceName={{this.model.type}}
@filters={{this.model}}
@reportOptions={{this.reportOptions}}
@showFilteringUI={{true}}
@onRefresh={{route-action "onParamsChange"}}
/>
<BackButton @route="adminReports" @label="admin.reports.back" />
<div class="admin-container admin-config-page__main-area">
<div class="admin-config-area">
<AdminReport
@dataSourceName={{this.model.type}}
@filters={{this.model}}
@reportOptions={{this.reportOptions}}
@showFilteringUI={{true}}
@onRefresh={{route-action "onParamsChange"}}
/>
</div>
</div>

View File

@ -100,11 +100,15 @@ acceptance("Admin Sidebar - Sections", function (needs) {
await click(".sidebar-toggle-all-sections");
await click(".sidebar-section-link[data-link-name='admin_all_reports']");
assert.dom(".admin-reports-list__report").exists({ count: 1 });
assert
.dom(".admin-reports-list .admin-section-landing-item__content")
.exists({ count: 1 });
await fillIn(".admin-reports-header__filter", "flags");
assert.dom(".admin-reports-list__report").doesNotExist();
assert
.dom(".admin-reports-list .admin-section-landing-item__content")
.doesNotExist();
await click(
".sidebar-section-link[data-link-name='admin_login_and_authentication']"
@ -112,13 +116,13 @@ acceptance("Admin Sidebar - Sections", function (needs) {
await click(".sidebar-section-link[data-link-name='admin_all_reports']");
assert
.dom(".admin-reports-list__report")
.dom(".admin-reports-list .admin-section-landing-item__content")
.exists({ count: 1 }, "navigating back and forth resets filter");
await fillIn(".admin-reports-header__filter", "activities");
assert
.dom(".admin-reports-list__report")
.dom(".admin-reports-list .admin-section-landing-item__content")
.exists({ count: 1 }, "filter is case insensitive");
});
});

View File

@ -73,23 +73,35 @@ acceptance("Dashboard", function (needs) {
await visit("/admin");
await click(".dashboard .navigation-item.reports .navigation-link");
assert.dom(".dashboard .admin-reports-list__report").exists({ count: 1 });
assert
.dom(
".dashboard .admin-reports-list .admin-section-landing-item__content"
)
.exists({ count: 1 });
await fillIn(".dashboard .admin-reports-header__filter", "flags");
assert.dom(".dashboard .admin-reports-list__report").doesNotExist();
assert
.dom(
".dashboard .admin-reports-list .admin-section-landing-item__content"
)
.doesNotExist();
await click(".dashboard .navigation-item.security .navigation-link");
await click(".dashboard .navigation-item.reports .navigation-link");
assert
.dom(".dashboard .admin-reports-list__report")
.dom(
".dashboard .admin-reports-list .admin-section-landing-item__content"
)
.exists({ count: 1 }, "navigating back and forth resets filter");
await fillIn(".dashboard .admin-reports-header__filter", "activities");
assert
.dom(".dashboard .admin-reports-list__report")
.dom(
".dashboard .admin-reports-list .admin-section-landing-item__content"
)
.exists({ count: 1 }, "filter is case insensitive");
});

View File

@ -8,15 +8,19 @@ acceptance("Reports", function (needs) {
test("Visit reports page", async function (assert) {
await visit("/admin/reports");
assert.dom(".admin-reports-list__report").exists({ count: 1 });
assert
.dom(".admin-reports-list .admin-section-landing-item__content")
.exists({ count: 1 });
assert
.dom(".admin-reports-list__report .admin-reports-list__report-title")
.dom(
".admin-reports-list .admin-section-landing-item__content .admin-section-landing-item__title"
)
.hasHtml("My report");
assert
.dom(
".admin-reports-list__report .admin-reports-list__report-description"
".admin-reports-list .admin-section-landing-item__content .admin-section-landing-item__description"
)
.hasHtml("List of my activities");
});

View File

@ -12,13 +12,15 @@ module("Integration | Component | admin-report", function (hooks) {
assert.dom(".admin-report.signups").exists();
assert.dom(".admin-report-table").exists("defaults to table mode");
assert.dom(".header .item.report").hasText("Signups", "has a title");
await click("[data-trigger]");
assert
.dom("[data-content]")
.hasText("New account registrations for this period");
.dom(".d-page-subheader .d-page-subheader__title")
.hasText("Signups", "has a title");
assert
.dom(".d-page-subheader .d-page-subheader__description")
.hasText(
"New account registrations for this period",
"has a description"
);
assert
.dom(".admin-report-table thead tr th:first-child .title")

View File

@ -73,6 +73,10 @@
flex-direction: column;
}
&__full-width {
flex: 1 0 100%;
}
&__primary-content {
flex: 0 1 70%;

View File

@ -79,6 +79,7 @@
.body {
display: flex;
flex-direction: row;
margin-top: var(--space-3);
}
.main {
@ -225,28 +226,32 @@
}
.admin-reports-list {
--d-border-radius: var(--space-0);
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin: 0 -1.5%;
&__report {
margin: 1.5%;
border: 1px solid var(--primary-low);
flex: 1 1 28%;
min-width: 225px;
max-width: 550px;
a {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 1em;
.report-description {
color: var(--primary-high);
&.admin-section-landing-wrapper {
gap: 1em;
padding-top: 0;
.admin-section-landing-item {
margin-bottom: 0;
padding: var(--space-4);
border-radius: var(--d-border-radius);
outline: 1px solid var(--primary-low);
margin-bottom: 0;
@include breakpoint("mobile-extra-large", min-width) {
margin-bottom: 0;
}
&:hover {
border: 0;
box-shadow: 0 0 0 1px var(--primary-300), 0 0 0 4px var(--primary-100);
transition: all 0.2s ease-in-out;
}
}
&:hover {
box-shadow: var(--shadow-card);
}
}
}

View File

@ -5136,8 +5136,9 @@ en:
reports:
title: "List of available reports"
meta_doc: "Explore our <a href='https://meta.discourse.org/t/-/240233' rel='noopener noreferrer' target='_blank'>documentation</a> for a detailed overview of the reports."
meta_doc: "Reports are a powerful tool to help you understand whats happening on your site. They can help you identify trends, spot problems, and make decisions based on data."
sidebar_title: "Reports"
back: "Back to all reports"
sidebar_link:
all: "All reports"