Skip to content
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

Usage/reports component and overall dashboard layout. (PP-1534) #126

Merged
merged 2 commits into from
Aug 22, 2024
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
11 changes: 9 additions & 2 deletions src/components/ContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ import { FeatureFlags, PathFor } from "../interfaces";
import Admin from "../models/Admin";
import PathForProvider from "@thepalaceproject/web-opds-client/lib/components/context/PathForContext";
import ActionCreator from "../actions";
import AppContextProvider from "../context/appContext";
import AppContextProvider, { AppContextType } from "../context/appContext";

// Note: Not all elements of these props make it into the `ContextProvider`.
// Some are exposed only through the `AppContextProvider` component (which
// this component wraps.
// TODO: We should get this interface to the point where we can just extend
// the `ConfigurationSettings` interface.
export interface ContextProviderProps extends React.Props<ContextProvider> {
store?: Store<RootState>;
csrfToken: string;
Expand All @@ -19,6 +24,7 @@ export interface ContextProviderProps extends React.Props<ContextProvider> {
library?: string;
}[];
featureFlags: FeatureFlags;
quicksightPagePath?: string;
}

/** Provides a redux store, configuration options, and a function to create URLs
Expand Down Expand Up @@ -97,11 +103,12 @@ export default class ContextProvider extends React.Component<
}

render() {
const appContextValue = {
const appContextValue: AppContextType = {
csrfToken: this.props.csrfToken,
settingUp: this.props.settingUp,
admin: this.admin,
featureFlags: this.props.featureFlags,
quicksightPagePath: this.props.quicksightPagePath,
};
return (
<PathForProvider pathFor={this.pathFor}>
Expand Down
69 changes: 46 additions & 23 deletions src/components/LibraryStats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import StatsTotalCirculationsGroup from "./StatsTotalCirculationsGroup";
import StatsPatronGroup from "./StatsPatronGroup";
import StatsInventoryGroup from "./StatsInventoryGroup";
import StatsCollectionsGroup from "./StatsCollectionsGroup";
import StatsUsageReportsGroup from "./StatsUsageReportsGroup";
import { useAppContext } from "../context/appContext";

export interface LibraryStatsProps {
stats: LibraryStatistics;
Expand Down Expand Up @@ -43,42 +45,63 @@ const LibraryStats = ({ stats, library }: LibraryStatsProps) => {
const inventoryReportRequestEnabled = useMayRequestInventoryReports({
library,
});
const dashboardTitle = library
? `${libraryName || libraryKey} Dashboard`
: ALL_LIBRARIES_HEADING;
const libraryOrLibraries = library ? "library's" : "libraries'";
const quicksightPageUrl = useAppContext().quicksightPagePath;

let statsLayoutClass: string, dashboardTitle: string, implementation: string;
if (library) {
dashboardTitle = `${libraryName || libraryKey} Dashboard`;
statsLayoutClass = "stats-with-library";
implementation = "library's implementation";
} else {
dashboardTitle = ALL_LIBRARIES_HEADING;
statsLayoutClass = "stats-without-library";
implementation = "libraries' implementations";
}
return (
<div className="library-stats">
<h2>{dashboardTitle}</h2>
<ul className="stats">
<li className="stat-group">
<ul className={`stats ${statsLayoutClass}`}>
<li className="stat-group stat-patrons-group">
<StatsPatronGroup
withActiveLoan={patrons.withActiveLoan}
withActiveLoanOrHold={patrons.withActiveLoanOrHold}
heading="Current Circulation Activity"
description="Real-time patron circulation information of the Palace System."
/>
</li>
<li className="stat-group">
<StatsTotalCirculationsGroup
{...patrons}
heading="Circulation Totals"
/>
</li>
<li className="stat-group">
<StatsInventoryGroup
library={library}
inventory={inventory}
inventoryReportsEnabled={inventoryReportRequestEnabled}
/>
</li>
<li className="stat-group stat-group-wide">
{!!library && (
<li className="stat-group stat-usage-reports-group">
<StatsUsageReportsGroup
library={library}
inventoryReportsEnabled={inventoryReportRequestEnabled}
usageDataTarget="_blank" // open in new tab or window
usageDataHref={quicksightPageUrl}
/>
</li>
)}
{!library && (
<>
<li className="stat-group stat-circulation-reports-group">
<StatsTotalCirculationsGroup
{...patrons}
heading="Circulation Totals"
/>
</li>
<li className="stat-group stat-inventory-reports-group">
<StatsInventoryGroup
library={library}
inventory={inventory}
inventoryReportsEnabled={inventoryReportRequestEnabled}
/>
</li>
</>
)}
<li className="stat-group stat-collections-group">
<StatsCollectionsGroup
heading={"Configured Collections"}
description={`
The following collections are configured in your ${libraryOrLibraries}
implementation of the Palace system and are available to your users
through the Palace app.
The following collections are configured in your ${implementation} of
the Palace system and are available to your users through the Palace app.
`}
collections={collections}
showBarChart={showBarChart}
Expand Down
84 changes: 84 additions & 0 deletions src/components/StatsUsageReportsGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React = require("react");
import { Button } from "library-simplified-reusable-components";
import StatsGroup from "./StatsGroup";
import { InventoryStatistics } from "../interfaces";
import InventoryReportRequestModal from "./InventoryReportRequestModal";
import { useState } from "react";
import { useAppContext } from "../context/appContext";

type Props = {
heading?: string;
description?: string;
inventoryReportsEnabled: boolean;
library?: string;
usageDataHref?: string;
usageDataLabel?: string;
usageDataTarget?: string;
};

const StatsUsageReportsGroup = ({
heading = "Usage and Reports",
description = `
Access historical circulation and usage data of the Palace system
and request inventory and holds reports to be sent via email.
`,
usageDataHref = "https://thepalaceproject.org",
usageDataLabel = "View Usage",
usageDataTarget = "_self",
inventoryReportsEnabled,
library = undefined,
}: Props) => {
const [showReportForm, setShowReportForm] = useState(false);

return (
<>
{inventoryReportsEnabled && library && (
<InventoryReportRequestModal
show={showReportForm}
onHide={() => setShowReportForm(false)}
library={library}
/>
)}
<ul className="stat-usage-reports">
<li>
<StatsGroup heading={heading} description={description}>
<>
{inventoryReportsEnabled && library && (
<Button
callback={(() => setShowReportForm(true)) as any}
content={
<>
Request Report &nbsp;&nbsp;
<i className="fa fa-regular fa-envelope" />
</>
}
title="Request an inventory report."
disabled={showReportForm}
/>
)}
<div className="stat-group-description">
These reports provide up-to-date data on both inventory and
holds for library at the time of the request.
</div>
</>
</StatsGroup>
</li>
<li>
<div className="stat-link">
<a
href={usageDataHref}
target={usageDataTarget}
rel="noopener noreferrer"
>
{usageDataLabel}
</a>
&nbsp;&nbsp;
<i className="fa fa-external-link" />
</div>
</li>
</ul>
</>
);
};

export default StatsUsageReportsGroup;
1 change: 1 addition & 0 deletions src/context/appContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type AppContextType = {
settingUp: boolean;
admin: Admin;
featureFlags: FeatureFlags;
quicksightPagePath: string;
};

// Don't export this, since we always want the error handling behavior of our hook.
Expand Down
8 changes: 6 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class CirculationAdmin {
const div = document.createElement("div");
div.id = "opds-catalog";
div.className = "palace";
config.featureFlags = { ...defaultFeatureFlags, ...config.featureFlags };
document.getElementsByTagName("body")[0].appendChild(div);

const catalogEditorPath =
Expand All @@ -36,10 +35,15 @@ class CirculationAdmin {
"/admin/web/lists(/:library)(/:editOrCreate)(/:identifier)";
const lanePagePath =
"/admin/web/lanes(/:library)(/:editOrCreate)(/:identifier)";
const quicksightPagePath = "/admin/web/quicksight";

const queryClient = new QueryClient();

const store = buildStore();

config.featureFlags = { ...defaultFeatureFlags, ...config.featureFlags };
config.quicksightPagePath = quicksightPagePath;

const appElement = "opds-catalog";
const app = config.settingUp ? (
<Provider store={store}>
Expand All @@ -63,7 +67,7 @@ class CirculationAdmin {
component={DashboardPage}
/>
<Route
path="/admin/web/quicksight"
path={quicksightPagePath}
component={QuicksightDashboardPage}
/>
<Route
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface ConfigurationSettings {
tos_link_href?: string;

featureFlags: FeatureFlags;

/** `quickSightPagePath` contains the URL to the QuickSight dashboard page.
Currently, this value does not change, so we can share it via fixed config. */
quicksightPagePath: string;
}

export interface FeatureFlags {
Expand Down
69 changes: 54 additions & 15 deletions src/stylesheets/stats.scss
Original file line number Diff line number Diff line change
@@ -1,19 +1,51 @@
.stats-with-library {
// With library, we show a two-column layout.
grid-template-columns: repeat(2, minmax(27rem, 1fr));
grid-template-areas:
"patrons usage-reports"
"collections collections"
;
}

.stats-without-library {
// Without a library ("all libraries"), we show three columns.
grid-template-columns: repeat(3, minmax(27rem, 1fr));
grid-template-areas:
"patrons circulations inventory"
"collections collections collections"
;
}

.stats {
display: grid;
grid-gap: 1rem 1rem;
grid-template-columns: repeat(auto-fit, minmax(27rem, 1fr));
grid-auto-rows: auto;
padding: 0;

.stat-group {
display: grid;
background-color: $pagecolorlight;
border-radius: 20px;
padding: 10px;
overflow-x: scroll;

&.stat-group-wide {
grid-column-start: 1;
grid-column-end: -1;
&.stat-patrons-group {
grid-area: patrons;
}

&.stat-circulations-group {
grid-area: circulations;
}

&.stat-inventory-group {
grid-area: inventory;
}

&.stat-usage-reports-group {
grid-area: usage-reports;
}

&.stat-collections-group {
grid-area: collections;

.recharts-wrapper {
// needed for Rechart ResponsiveContainer to shrink performantly with parent
Expand All @@ -39,13 +71,29 @@

.stat-group-description {
margin: 10px;
margin-top: 0px;
margin-top: 0;
margin-bottom: 20px;
text-align: left;
font-style: italic;
font-size: small;
}

.stat-usage-reports {
display: grid;
grid-gap: 2rem;
grid-template-columns: 10fr minmax(8rem, 1fr);
list-style-type: none;
}

.stat-link {
margin-top: 1.5rem;
color: $blue-dark;

a {
color: $blue-dark;
text-decoration: underline;
}
}

ul {
padding: 0;
Expand All @@ -57,15 +105,6 @@
margin: 10px;
margin-bottom: 2px;
}
h4 {
margin: 10px;
margin-top: 0px;
margin-bottom: 20px;
text-align: left;
font-style: italic;
font-size: small;
}

}

.stat-grouping-label {
Expand Down
Loading