Skip to content

Commit

Permalink
Logo-only header for QuickSight page. (PP-1720) (#136)
Browse files Browse the repository at this point in the history
* Add assett and style mocks to jest, so we can import our logo in tests.

* Add terms of service context to our providers test helper.

* Enable a logo-only header and use it for QuickSight dashboard.
  • Loading branch information
tdilauro authored Sep 27, 2024
1 parent a83177a commit 2dba4e5
Show file tree
Hide file tree
Showing 8 changed files with 139 additions and 81 deletions.
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
moduleNameMapper: {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"<rootDir>/tests/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/tests/__mocks__/styleMock.js",
},
preset: "ts-jest",
testEnvironment: "jsdom",
testEnvironmentOptions: {
Expand Down
153 changes: 86 additions & 67 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface HeaderDispatchProps {

export interface HeaderOwnProps {
store?: Store<RootState>;
logoOnly?: boolean;
}

export interface HeaderProps
Expand Down Expand Up @@ -130,83 +131,90 @@ export class Header extends React.Component<HeaderProps, HeaderState> {
},
];
const accountLink = { label: "Change password", href: "account/" };
const logoOnly = this.props.logoOnly ?? false;

return (
<Navbar fluid={true}>
<Navbar.Header>
<img src={palaceLogoUrl} alt={title()} />
{this.props.libraries && this.props.libraries.length > 0 && (
<EditableInput
elementType="select"
ref={this.libraryRef}
value={currentLibrary}
onChange={this.changeLibrary}
aria-label="Select a library"
>
{(!this.context.library || !currentLibrary) && (
<option aria-selected={false}>Select a library</option>
)}
{this.props.libraries.map((library) => (
<option
key={library.short_name}
value={library.short_name}
aria-selected={currentLibrary === library.short_name}
{!logoOnly && (
<>
{this.props.libraries && this.props.libraries.length > 0 && (
<EditableInput
elementType="select"
ref={this.libraryRef}
value={currentLibrary}
onChange={this.changeLibrary}
aria-label="Select a library"
>
{library.name || library.short_name}
</option>
))}
</EditableInput>
{(!this.context.library || !currentLibrary) && (
<option aria-selected={false}>Select a library</option>
)}
{this.props.libraries.map((library) => (
<option
key={library.short_name}
value={library.short_name}
aria-selected={currentLibrary === library.short_name}
>
{library.name || library.short_name}
</option>
))}
</EditableInput>
)}
<Navbar.Toggle />
</>
)}
<Navbar.Toggle />
</Navbar.Header>

<Navbar.Collapse className="menu">
{currentLibrary && (
<Nav>
{this.renderLinkItem(
dashboardLinkItem,
currentPathname,
currentLibrary
)}
{libraryNavItems.map((item) =>
this.renderNavItem(item, currentPathname, currentLibrary)
{!logoOnly && (
<Navbar.Collapse className="menu">
{currentLibrary && (
<Nav>
{this.renderLinkItem(
dashboardLinkItem,
currentPathname,
currentLibrary
)}
{libraryNavItems.map((item) =>
this.renderNavItem(item, currentPathname, currentLibrary)
)}
{libraryLinkItems.map((item) =>
this.renderLinkItem(item, currentPathname, currentLibrary)
)}
</Nav>
)}
<Nav className="pull-right">
{sitewideLinkItems.map((item) =>
this.renderLinkItem(item, currentPathname)
)}
{libraryLinkItems.map((item) =>
this.renderLinkItem(item, currentPathname, currentLibrary)
{this.context.admin.email && (
<li className="dropdown">
<Button
className="account-dropdown-toggle transparent"
type="button"
aria-haspopup="true"
aria-expanded={this.state.showAccountDropdown}
callback={this.toggleAccountDropdown}
content={
<span>
{this.context.admin.email} <GenericWedgeIcon />
</span>
}
/>
{this.state.showAccountDropdown && (
<ul className="dropdown-menu">
{this.displayPermissions(isSystemAdmin, isLibraryManager)}
{this.renderLinkItem(accountLink, currentPathname)}
<li>
<a href="/admin/sign_out">Sign out</a>
</li>
</ul>
)}
</li>
)}
</Nav>
)}
<Nav className="pull-right">
{sitewideLinkItems.map((item) =>
this.renderLinkItem(item, currentPathname)
)}
{this.context.admin.email && (
<li className="dropdown">
<Button
className="account-dropdown-toggle transparent"
type="button"
aria-haspopup="true"
aria-expanded={this.state.showAccountDropdown}
callback={this.toggleAccountDropdown}
content={
<span>
{this.context.admin.email} <GenericWedgeIcon />
</span>
}
/>
{this.state.showAccountDropdown && (
<ul className="dropdown-menu">
{this.displayPermissions(isSystemAdmin, isLibraryManager)}
{this.renderLinkItem(accountLink, currentPathname)}
<li>
<a href="/admin/sign_out">Sign out</a>
</li>
</ul>
)}
</li>
)}
</Nav>
</Navbar.Collapse>
</Navbar.Collapse>
)}
</Navbar>
);
}
Expand Down Expand Up @@ -331,14 +339,25 @@ const ConnectedHeader = connect<

/** HeaderWithStore is a wrapper component to pass the store as a prop to the
ConnectedHeader, since it's not in the regular place in the context. */
export default class HeaderWithStore extends React.Component<{}, {}> {
type HeaderWithStoreProps = {
logoOnly?: boolean;
};

export default class HeaderWithStore extends React.Component<
HeaderWithStoreProps
> {
context: { editorStore: Store<RootState> };

static contextTypes = {
editorStore: PropTypes.object.isRequired,
};

render(): JSX.Element {
return <ConnectedHeader store={this.context.editorStore} />;
return (
<ConnectedHeader
store={this.context.editorStore}
logoOnly={this.props.logoOnly}
/>
);
}
}
2 changes: 1 addition & 1 deletion src/components/QuicksightDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default class QuicksightDashboardPage extends React.Component<
const { library } = this.props.params;
return (
<div className="quicksight-dashboard">
<Header />
<Header logoOnly={true} />
<main className="body">
<QuicksightDashboard dashboardId="library" />
</main>
Expand Down
7 changes: 6 additions & 1 deletion src/components/TOSContext.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import * as React from "react";
export const TOSContext = React.createContext(null);

export type TOSContextProviderProps = {
[key: number]: string;
};

export const TOSContext = React.createContext<TOSContextProviderProps>(null);
export const TOSContextProvider = TOSContext.Provider;
1 change: 1 addition & 0 deletions tests/__mocks__/fileMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = "test-file-stub";
1 change: 1 addition & 0 deletions tests/__mocks__/styleMock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = {};
29 changes: 20 additions & 9 deletions tests/jest/components/QuicksightDashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import buildStore from "../../../src/store";
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
import renderWithContext from "../testUtils/renderWithContext";
import { renderWithProviders } from "../testUtils/withProviders";
import QuicksightDashboardPage from "../../../src/components/QuicksightDashboardPage";

const libraries: LibrariesData = { libraries: [{ uuid: "my-uuid" }] };
const dashboardId = "test";
Expand Down Expand Up @@ -35,15 +37,8 @@ describe("QuicksightDashboard", () => {
});

it("embed url is retrieved and set in iframe", async () => {
const contextProviderProps = {
csrfToken: "",
featureFlags: {},
roles: [{ role: "system" }],
};

renderWithContext(
<QuicksightDashboard dashboardId={dashboardId} store={buildStore()} />,
contextProviderProps
renderWithProviders(
<QuicksightDashboard dashboardId={dashboardId} store={buildStore()} />
);

await waitFor(() => {
Expand All @@ -53,4 +48,20 @@ describe("QuicksightDashboard", () => {
);
});
});

it("header renders without navigation links ", () => {
renderWithProviders(<QuicksightDashboardPage params={{ library: null }} />);

// Make sure we see the QuicksSight iFrame.
expect(screen.getByTitle("Library Dashboard")).toBeInTheDocument();
// Make sure we have the branding image.
expect(
screen.getByAltText("Palace Collection Manager")
).toBeInTheDocument();

// Make sure we do not see other navigation links.
["Dashboard", "System Configuration"].forEach((name) => {
expect(screen.queryByText(name)).not.toBeInTheDocument();
});
});
});
22 changes: 19 additions & 3 deletions tests/jest/testUtils/withProviders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, RenderOptions, RenderResult } from "@testing-library/react";
import { defaultFeatureFlags } from "../../../src/utils/featureFlags";
import { store } from "../../../src/store";
import {
TOSContextProvider,
TOSContextProviderProps,
} from "../../../src/components/TOSContext";

export type TestProviderWrapperOptions = {
reduxProviderProps?: ProviderProps;
contextProviderProps?: Partial<ContextProviderProps>;
tosContextProviderProps?: TOSContextProviderProps;
queryClient?: QueryClient;
};
export type TestRenderWrapperOptions = TestProviderWrapperOptions & {
Expand All @@ -21,6 +26,13 @@ export type TestRenderWrapperOptions = TestProviderWrapperOptions & {
// be the same for both the Redux Provider and the ContextProvider.
const defaultReduxStore = store;

// Setup default TOSContext provider props.
const tosText = "Sample terms of service.";
const tosHref = "http://example.com/terms-of-service";
const requiredTOSContextProviderProps: TOSContextProviderProps = {
...[tosText, tosHref],
};

// The `csrfToken` context provider prop is required, so we provide
// a default value here, so it can be easily merged with other props.
const requiredContextProviderProps: ContextProviderProps = {
Expand All @@ -35,6 +47,7 @@ const requiredContextProviderProps: ContextProviderProps = {
* @param {TestProviderWrapperOptions} options
* @param options.reduxProviderProps Props to pass to the Redux `Provider` wrapper
* @param {ContextProviderProps} options.contextProviderProps Props to pass to the ContextProvider wrapper
* @param {TOSContextProviderProps} options.tosContextProviderProps Props to pass to the TOSContextProvider wrapper
* @param {QueryClient} options.queryClient A `tanstack/react-query` QueryClient
* @returns {React.FunctionComponent} A React component that wraps children with our providers
*/
Expand All @@ -46,6 +59,7 @@ export const componentWithProviders = ({
csrfToken: "",
featureFlags: defaultFeatureFlags,
},
tosContextProviderProps = requiredTOSContextProviderProps,
queryClient = new QueryClient(),
}: TestProviderWrapperOptions = {}): React.FunctionComponent => {
const effectiveContextProviderProps = {
Expand All @@ -56,9 +70,11 @@ export const componentWithProviders = ({
const wrapper = ({ children }) => (
<Provider {...reduxProviderProps}>
<ContextProvider {...effectiveContextProviderProps}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
<TOSContextProvider value={tosContextProviderProps}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</TOSContextProvider>
</ContextProvider>
</Provider>
);
Expand Down

0 comments on commit 2dba4e5

Please sign in to comment.