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

Logo-only header for QuickSight page. (PP-1720) #136

Merged
merged 3 commits into from
Sep 27, 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
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