├── .editorconfig ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ ├── fileMock.js └── styleMock.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── AppLayout │ │ └── AppLayout.tsx │ ├── Dashboard │ │ └── Dashboard.tsx │ ├── NotFound │ │ └── NotFound.tsx │ ├── Settings │ │ ├── General │ │ │ └── GeneralSettings.tsx │ │ └── Profile │ │ │ └── ProfileSettings.tsx │ ├── Support │ │ └── Support.tsx │ ├── __snapshots__ │ │ └── app.test.tsx.snap │ ├── app.css │ ├── app.test.tsx │ ├── bgimages │ │ └── Patternfly-Logo.svg │ ├── index.tsx │ ├── routes.tsx │ └── utils │ │ └── useDocumentTitle.ts ├── favicon.png ├── index.html ├── index.tsx └── typings.d.ts ├── stylePaths.js ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.snap] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // tells eslint to use the TypeScript parser 3 | "parser": "@typescript-eslint/parser", 4 | // tell the TypeScript parser that we want to use JSX syntax 5 | "parserOptions": { 6 | "tsx": true, 7 | "jsx": true, 8 | "js": true, 9 | "useJSXTextNode": true, 10 | "project": "./tsconfig.json", 11 | "tsconfigRootDir": "." 12 | }, 13 | // we want to use the recommended rules provided from the typescript plugin 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:react/recommended", 17 | "plugin:@typescript-eslint/recommended" 18 | ], 19 | "globals": { 20 | "window": "readonly", 21 | "describe": "readonly", 22 | "test": "readonly", 23 | "expect": "readonly", 24 | "it": "readonly", 25 | "process": "readonly", 26 | "document": "readonly", 27 | "insights": "readonly", 28 | "shallow": "readonly", 29 | "render": "readonly", 30 | "mount": "readonly" 31 | }, 32 | "overrides": [ 33 | { 34 | "files": ["src/**/*.ts", "src/**/*.tsx"], 35 | "parser": "@typescript-eslint/parser", 36 | "plugins": ["@typescript-eslint"], 37 | "extends": ["plugin:@typescript-eslint/recommended"], 38 | "rules": { 39 | "react/prop-types": "off", 40 | "@typescript-eslint/no-unused-vars": "error" 41 | }, 42 | }, 43 | ], 44 | "settings": { 45 | "react": { 46 | "version": "^16.11.0" 47 | } 48 | }, 49 | // includes the typescript specific rules found here: https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules 50 | "plugins": [ 51 | "@typescript-eslint", 52 | "react-hooks", 53 | "eslint-plugin-react-hooks" 54 | ], 55 | "rules": { 56 | "sort-imports": [ 57 | "error", 58 | { 59 | "ignoreDeclarationSort": true 60 | } 61 | ], 62 | "@typescript-eslint/explicit-function-return-type": "off", 63 | "react-hooks/rules-of-hooks": "error", 64 | "react-hooks/exhaustive-deps": "warn", 65 | "@typescript-eslint/interface-name-prefix": "off", 66 | "prettier/prettier": "off", 67 | "import/no-unresolved": "off", 68 | "import/extensions": "off", 69 | "react/prop-types": "off" 70 | }, 71 | "env": { 72 | "browser": true, 73 | "node": true 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the react seed 4 | title: '' 5 | labels: needs triage 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Additional context** 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | - release* 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: lts/* 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Run eslint 23 | run: npm run lint 24 | test: 25 | name: Test 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | - name: Setup Node.js 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: lts/* 34 | - name: Install dependencies 35 | run: npm install 36 | - name: Run tests 37 | run: npm run test 38 | build: 39 | name: Build 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | - name: Setup Node.js 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: lts/* 48 | - name: Install dependencies 49 | run: npm install 50 | - name: Attempt a build 51 | run: npm run build 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | dist 3 | yarn-error.log 4 | yarn.lock 5 | stats.json 6 | coverage 7 | storybook-static 8 | .DS_Store 9 | .idea 10 | .env 11 | .history 12 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 120 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Red Hat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Patternfly Seed 2 | 3 | Patternfly Seed is an open source build scaffolding utility for web apps. The primary purpose of this project is to give developers a jump start when creating new projects that will use patternfly. A secondary purpose of this project is to serve as a reference for how to configure various aspects of an application that uses patternfly, webpack, react, typescript, etc. 4 | 5 | Out of the box you'll get an app layout with chrome (header/sidebar), routing, build pipeline, test suite, and some code quality tools. Basically, all the essentials. 6 | 7 | Out of box dashboard view of patternfly seed 8 | 9 | ## Quick-start 10 | 11 | ```bash 12 | git clone https://github.com/patternfly/patternfly-react-seed 13 | cd patternfly-react-seed 14 | npm install && npm run start:dev 15 | ``` 16 | ## Development scripts 17 | ```sh 18 | # Install development/build dependencies 19 | npm install 20 | 21 | # Start the development server 22 | npm run start:dev 23 | 24 | # Run a production build (outputs to "dist" dir) 25 | npm run build 26 | 27 | # Run the test suite 28 | npm run test 29 | 30 | # Run the test suite with coverage 31 | npm run test:coverage 32 | 33 | # Run the linter 34 | npm run lint 35 | 36 | # Run the code formatter 37 | npm run format 38 | 39 | # Launch a tool to inspect the bundle size 40 | npm run bundle-profile:analyze 41 | 42 | # Start the express server (run a production build first) 43 | npm run start 44 | ``` 45 | 46 | ## Configurations 47 | * [TypeScript Config](./tsconfig.json) 48 | * [Webpack Config](./webpack.common.js) 49 | * [Jest Config](./jest.config.js) 50 | * [Editor Config](./.editorconfig) 51 | 52 | ## Raster image support 53 | 54 | To use an image asset that's shipped with PatternFly core, you'll prefix the paths with "@assets". `@assets` is an alias for the PatternFly assets directory in node_modules. 55 | 56 | For example: 57 | ```js 58 | import imgSrc from '@assets/images/g_sizing.png'; 59 | Some image 60 | ``` 61 | 62 | You can use a similar technique to import assets from your local app, just prefix the paths with "@app". `@app` is an alias for the main src/app directory. 63 | 64 | ```js 65 | import loader from '@app/assets/images/loader.gif'; 66 | Content loading 67 | ``` 68 | 69 | ## Vector image support 70 | Inlining SVG in the app's markup is also possible. 71 | 72 | ```js 73 | import logo from '@app/assets/images/logo.svg'; 74 | 75 | ``` 76 | 77 | You can also use SVG when applying background images with CSS. To do this, your SVG's must live under a `bgimages` directory (this directory name is configurable in [webpack.common.js](./webpack.common.js#L5)). This is necessary because you may need to use SVG's in several other context (inline images, fonts, icons, etc.) and so we need to be able to differentiate between these usages so the appropriate loader is invoked. 78 | ```css 79 | body { 80 | background: url(./assets/bgimages/img_avatar.svg); 81 | } 82 | ``` 83 | 84 | ## Adding custom CSS 85 | When importing CSS from a third-party package for the first time, you may encounter the error `Module parse failed: Unexpected token... You may need an appropriate loader to handle this file typ...`. You need to register the path to the stylesheet directory in [stylePaths.js](./stylePaths.js). We specify these explicitly for performance reasons to avoid webpack needing to crawl through the entire node_modules directory when parsing CSS modules. 86 | 87 | ## Code quality tools 88 | * For accessibility compliance, we use [react-axe](https://github.com/dequelabs/react-axe) 89 | * To keep our bundle size in check, we use [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) 90 | * To keep our code formatting in check, we use [prettier](https://github.com/prettier/prettier) 91 | * To keep our code logic and test coverage in check, we use [jest](https://github.com/facebook/jest) 92 | * To ensure code styles remain consistent, we use [eslint](https://eslint.org/) 93 | 94 | ## Multi environment configuration 95 | This project uses [dotenv-webpack](https://www.npmjs.com/package/dotenv-webpack) for exposing environment variables to your code. Either export them at the system level like `export MY_ENV_VAR=http://dev.myendpoint.com && npm run start:dev` or simply drop a `.env` file in the root that contains your key-value pairs like below: 96 | 97 | ```sh 98 | ENV_1=http://1.myendpoint.com 99 | ENV_2=http://2.myendpoint.com 100 | ``` 101 | 102 | 103 | With that in place, you can use the values in your code like `console.log(process.env.ENV_1);` 104 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // Automatically clear mock calls and instances between every test 6 | clearMocks: true, 7 | 8 | // Indicates whether the coverage information should be collected while executing the test 9 | collectCoverage: false, 10 | 11 | // The directory where Jest should output its coverage files 12 | coverageDirectory: 'coverage', 13 | 14 | // An array of directory names to be searched recursively up from the requiring module's location 15 | moduleDirectories: [ 16 | "node_modules", 17 | "/src" 18 | ], 19 | 20 | // A map from regular expressions to module names that allow to stub out resources with a single module 21 | moduleNameMapper: { 22 | '\\.(css|less)$': '/__mocks__/styleMock.js', 23 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 24 | "@app/(.*)": '/src/app/$1' 25 | }, 26 | 27 | // A preset that is used as a base for Jest's configuration 28 | preset: "ts-jest/presets/js-with-ts", 29 | 30 | // The test environment that will be used for testing. 31 | testEnvironment: "jest-fixed-jsdom", 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "patternfly-seed", 3 | "version": "0.0.2", 4 | "description": "An open source build scaffolding utility for web apps.", 5 | "repository": "https://github.com/patternfly/patternfly-react-seed.git", 6 | "homepage": "https://patternfly-react-seed.surge.sh", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "prebuild": "npm run type-check && npm run clean", 11 | "build": "webpack --config webpack.prod.js", 12 | "start": "sirv dist --cors --single --host --port 8080", 13 | "start:dev": "webpack serve --color --progress --config webpack.dev.js", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:coverage": "jest --coverage", 17 | "eslint": "eslint --ext .tsx,.js ./src/", 18 | "lint": "npm run eslint", 19 | "format": "prettier --check --write ./src/**/*.{tsx,ts}", 20 | "type-check": "tsc --noEmit", 21 | "ci-checks": "npm run type-check && npm run lint && npm run test:coverage", 22 | "build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json", 23 | "bundle-profile:analyze": "npm run build:bundle-profile && webpack-bundle-analyzer ./stats.json", 24 | "clean": "rimraf dist" 25 | }, 26 | "devDependencies": { 27 | "@testing-library/jest-dom": "^6.6.3", 28 | "@testing-library/react": "^16.2.0", 29 | "@testing-library/user-event": "^14.6.1", 30 | "@types/jest": "^29.5.14", 31 | "@types/react-router-dom": "^5.3.3", 32 | "@typescript-eslint/eslint-plugin": "^8.17.0", 33 | "@typescript-eslint/parser": "^8.17.0", 34 | "copy-webpack-plugin": "^12.0.2", 35 | "css-loader": "^7.1.2", 36 | "css-minimizer-webpack-plugin": "^7.0.0", 37 | "dotenv-webpack": "^8.1.0", 38 | "eslint": "^8.57.1", 39 | "eslint-plugin-prettier": "^5.2.1", 40 | "eslint-plugin-react": "^7.37.2", 41 | "eslint-plugin-react-hooks": "^5.0.0", 42 | "html-webpack-plugin": "^5.6.3", 43 | "imagemin": "^9.0.0", 44 | "jest-environment-jsdom": "^29.7.0", 45 | "jest-fixed-jsdom": "^0.0.9", 46 | "mini-css-extract-plugin": "^2.9.2", 47 | "postcss": "^8.4.49", 48 | "prettier": "^3.4.2", 49 | "prop-types": "^15.8.1", 50 | "raw-loader": "^4.0.2", 51 | "react-axe": "^3.5.4", 52 | "react-docgen-typescript-loader": "^3.7.2", 53 | "react-router-dom": "^7.0.2", 54 | "regenerator-runtime": "^0.14.1", 55 | "rimraf": "^6.0.1", 56 | "style-loader": "^4.0.0", 57 | "svg-url-loader": "^8.0.0", 58 | "terser-webpack-plugin": "^5.3.10", 59 | "ts-jest": "^29.2.5", 60 | "ts-loader": "^9.5.1", 61 | "tsconfig-paths-webpack-plugin": "^4.2.0", 62 | "tslib": "^2.8.1", 63 | "typescript": "^5.7.2", 64 | "url-loader": "^4.1.1", 65 | "webpack": "^5.97.0", 66 | "webpack-bundle-analyzer": "^4.10.2", 67 | "webpack-cli": "^5.1.4", 68 | "webpack-dev-server": "^5.1.0", 69 | "webpack-merge": "^6.0.1" 70 | }, 71 | "dependencies": { 72 | "@patternfly/react-core": "^6.0.0", 73 | "@patternfly/react-icons": "^6.0.0", 74 | "@patternfly/react-styles": "^6.0.0", 75 | "react": "^18", 76 | "react-dom": "^18", 77 | "sirv-cli": "^3.0.0" 78 | }, 79 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 80 | } 81 | -------------------------------------------------------------------------------- /src/app/AppLayout/AppLayout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { NavLink, useLocation } from 'react-router-dom'; 3 | import { 4 | Button, 5 | Masthead, 6 | MastheadBrand, 7 | MastheadLogo, 8 | MastheadMain, 9 | MastheadToggle, 10 | Nav, 11 | NavExpandable, 12 | NavItem, 13 | NavList, 14 | Page, 15 | PageSidebar, 16 | PageSidebarBody, 17 | SkipToContent, 18 | } from '@patternfly/react-core'; 19 | import { IAppRoute, IAppRouteGroup, routes } from '@app/routes'; 20 | import { BarsIcon } from '@patternfly/react-icons'; 21 | 22 | interface IAppLayout { 23 | children: React.ReactNode; 24 | } 25 | 26 | const AppLayout: React.FunctionComponent = ({ children }) => { 27 | const [sidebarOpen, setSidebarOpen] = React.useState(true); 28 | const masthead = ( 29 | 30 | 31 | 32 | 20 | ); 21 | } 22 | 23 | return ( 24 | 25 | 26 | 27 | We didn't find a page that matches the address you navigated to. 28 | 29 | 30 | 31 | 32 | ) 33 | }; 34 | 35 | export { NotFound }; 36 | -------------------------------------------------------------------------------- /src/app/Settings/General/GeneralSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PageSection, Title } from '@patternfly/react-core'; 3 | import { useDocumentTitle } from '@app/utils/useDocumentTitle'; 4 | 5 | const GeneralSettings: React.FunctionComponent = () => { 6 | useDocumentTitle("General Settings"); 7 | return ( 8 | 9 | 10 | General Settings Page Title 11 | 12 | 13 | ); 14 | } 15 | 16 | export { GeneralSettings }; 17 | -------------------------------------------------------------------------------- /src/app/Settings/Profile/ProfileSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PageSection, Title } from '@patternfly/react-core'; 3 | import { useDocumentTitle } from '@app/utils/useDocumentTitle'; 4 | 5 | const ProfileSettings: React.FunctionComponent = () => { 6 | useDocumentTitle("Profile Settings"); 7 | 8 | return ( 9 | 10 | 11 | Profile Settings Page Title 12 | 13 | 14 | ); 15 | 16 | } 17 | 18 | export { ProfileSettings }; 19 | -------------------------------------------------------------------------------- /src/app/Support/Support.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { CubesIcon } from '@patternfly/react-icons'; 3 | import { 4 | Button, 5 | Content, 6 | ContentVariants, 7 | EmptyState, 8 | EmptyStateActions, 9 | EmptyStateBody, 10 | EmptyStateFooter, 11 | EmptyStateVariant, 12 | PageSection, 13 | } from '@patternfly/react-core'; 14 | 15 | export interface ISupportProps { 16 | sampleProp?: string; 17 | } 18 | 19 | // eslint-disable-next-line prefer-const 20 | let Support: React.FunctionComponent = () => ( 21 | 22 | 23 | 24 | 25 | 26 | This represents an the empty state pattern in Patternfly. Hopefully it's simple enough to use but 27 | flexible enough to meet a variety of needs. 28 | 29 | 30 | This text has overridden a css component variable to demonstrate how to apply customizations using 31 | PatternFly's CSS tokens. 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | export { Support }; 51 | -------------------------------------------------------------------------------- /src/app/__snapshots__/app.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App tests should render default App component 1`] = ` 4 | 5 |
8 | 26 |
29 |
32 | 35 | 62 | 63 |
67 | 159 |
160 |
161 |
162 |
167 |
170 | 288 |
289 |
290 |
293 |
298 |
301 |

307 | Dashboard Page Title! 308 |

309 |
310 |
311 |
312 |
313 |
314 | `; 315 | -------------------------------------------------------------------------------- /src/app/app.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | 7 | .pf-v6-c-content { 8 | --pf-v6-c-content--small--Color: var(--pf-t--global--color--status--danger--default); /* changes all color to the semantic token for danger */ 9 | --pf-v6-c-content--blockquote--BorderLeftColor: var(--pf-t--global--color--nonstatus--purple--default); /* changes all
left border color to the semantic token for non-status (purple) */ 10 | --pf-v6-c-content--hr--BackgroundColor: var(--pf-t--global--color--nonstatus--yellow--default); /* changes a
color to the semantic token for non-status (yellow) */ 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import App from '@app/index'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import '@testing-library/jest-dom'; 6 | 7 | describe('App tests', () => { 8 | test('should render default App component', () => { 9 | const { asFragment } = render(); 10 | 11 | expect(asFragment()).toMatchSnapshot(); 12 | }); 13 | 14 | it('should render a nav-toggle button', () => { 15 | render(); 16 | 17 | expect(screen.getByRole('button', { name: 'Global navigation' })).toBeVisible(); 18 | }); 19 | 20 | // I'm fairly sure that this test not going to work properly no matter what we do since JSDOM doesn't actually 21 | // draw anything. We could potentially make something work, likely using a different test environment, but 22 | // using Cypress for this kind of test would be more efficient. 23 | it.skip('should hide the sidebar on smaller viewports', () => { 24 | Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 600 }); 25 | 26 | render(); 27 | 28 | window.dispatchEvent(new Event('resize')); 29 | 30 | expect(screen.queryByRole('link', { name: 'Dashboard' })).not.toBeInTheDocument(); 31 | }); 32 | 33 | it('should expand the sidebar on larger viewports', () => { 34 | render(); 35 | 36 | window.dispatchEvent(new Event('resize')); 37 | 38 | expect(screen.getByRole('link', { name: 'Dashboard' })).toBeVisible(); 39 | }); 40 | 41 | it('should hide the sidebar when clicking the nav-toggle button', async () => { 42 | const user = userEvent.setup(); 43 | 44 | render(); 45 | 46 | window.dispatchEvent(new Event('resize')); 47 | const button = screen.getByRole('button', { name: 'Global navigation' }); 48 | 49 | expect(screen.getByRole('link', { name: 'Dashboard' })).toBeVisible(); 50 | 51 | await user.click(button); 52 | 53 | expect(screen.queryByRole('link', { name: 'Dashboard' })).not.toBeInTheDocument(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/bgimages/Patternfly-Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Patternfly Logo 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '@patternfly/react-core/dist/styles/base.css'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { AppLayout } from '@app/AppLayout/AppLayout'; 5 | import { AppRoutes } from '@app/routes'; 6 | import '@app/app.css'; 7 | 8 | const App: React.FunctionComponent = () => ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/app/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import { Dashboard } from '@app/Dashboard/Dashboard'; 4 | import { Support } from '@app/Support/Support'; 5 | import { GeneralSettings } from '@app/Settings/General/GeneralSettings'; 6 | import { ProfileSettings } from '@app/Settings/Profile/ProfileSettings'; 7 | import { NotFound } from '@app/NotFound/NotFound'; 8 | 9 | export interface IAppRoute { 10 | label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout 11 | /* eslint-disable @typescript-eslint/no-explicit-any */ 12 | element: React.ReactElement; 13 | /* eslint-enable @typescript-eslint/no-explicit-any */ 14 | exact?: boolean; 15 | path: string; 16 | title: string; 17 | routes?: undefined; 18 | } 19 | 20 | export interface IAppRouteGroup { 21 | label: string; 22 | routes: IAppRoute[]; 23 | } 24 | 25 | export type AppRouteConfig = IAppRoute | IAppRouteGroup; 26 | 27 | const routes: AppRouteConfig[] = [ 28 | { 29 | element: , 30 | exact: true, 31 | label: 'Dashboard', 32 | path: '/', 33 | title: 'PatternFly Seed | Main Dashboard', 34 | }, 35 | { 36 | element: , 37 | exact: true, 38 | label: 'Support', 39 | path: '/support', 40 | title: 'PatternFly Seed | Support Page', 41 | }, 42 | { 43 | label: 'Settings', 44 | routes: [ 45 | { 46 | element: , 47 | exact: true, 48 | label: 'General', 49 | path: '/settings/general', 50 | title: 'PatternFly Seed | General Settings', 51 | }, 52 | { 53 | element: , 54 | exact: true, 55 | label: 'Profile', 56 | path: '/settings/profile', 57 | title: 'PatternFly Seed | Profile Settings', 58 | }, 59 | ], 60 | }, 61 | ]; 62 | 63 | const flattenedRoutes: IAppRoute[] = routes.reduce( 64 | (flattened, route) => [...flattened, ...(route.routes ? route.routes : [route])], 65 | [] as IAppRoute[], 66 | ); 67 | 68 | const AppRoutes = (): React.ReactElement => ( 69 | 70 | {flattenedRoutes.map(({ path, element }, idx) => ( 71 | 72 | ))} 73 | } /> 74 | 75 | ); 76 | 77 | export { AppRoutes, routes }; 78 | -------------------------------------------------------------------------------- /src/app/utils/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // a custom hook for setting the page title 4 | export function useDocumentTitle(title: string) { 5 | React.useEffect(() => { 6 | const originalTitle = document.title; 7 | document.title = title; 8 | 9 | return () => { 10 | document.title = originalTitle; 11 | }; 12 | }, [title]); 13 | } 14 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patternfly/patternfly-react-seed/72362947d44debca429fc1cebb278437627c63a8/src/favicon.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Patternfly Seed 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from '@app/index'; 4 | 5 | if (process.env.NODE_ENV !== "production") { 6 | const config = { 7 | rules: [ 8 | { 9 | id: 'color-contrast', 10 | enabled: false 11 | } 12 | ] 13 | }; 14 | // eslint-disable-next-line @typescript-eslint/no-require-imports 15 | const axe = require("react-axe"); 16 | axe(React, ReactDOM, 1000, config); 17 | } 18 | 19 | const root = ReactDOM.createRoot(document.getElementById("root") as Element); 20 | 21 | root.render( 22 | 23 | 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.jpeg'; 4 | declare module '*.gif'; 5 | declare module '*.svg'; 6 | declare module '*.css'; 7 | declare module '*.wav'; 8 | declare module '*.mp3'; 9 | declare module '*.m4a'; 10 | declare module '*.rdf'; 11 | declare module '*.ttl'; 12 | declare module '*.pdf'; 13 | -------------------------------------------------------------------------------- /stylePaths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stylePaths: [ 4 | path.resolve(__dirname, 'src'), 5 | path.resolve(__dirname, 'node_modules/patternfly'), 6 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), 7 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), 8 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), 9 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), 10 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), 11 | path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), 12 | path.resolve(__dirname, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css') 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es6", "dom"], 9 | "sourceMap": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "allowJs": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "paths": { 21 | "@app/*": ["src/app/*"], 22 | "@assets/*": ["node_modules/@patternfly/react-core/dist/styles/assets/*"] 23 | }, 24 | "importHelpers": true, 25 | "skipLibCheck": true 26 | }, 27 | "include": [ 28 | "**/*.ts", 29 | "**/*.tsx", 30 | "**/*.jsx", 31 | "**/*.js" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 7 | const Dotenv = require('dotenv-webpack'); 8 | const BG_IMAGES_DIRNAME = 'bgimages'; 9 | const ASSET_PATH = process.env.ASSET_PATH || '/'; 10 | module.exports = (env) => { 11 | return { 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(tsx|ts|jsx)?$/, 16 | use: [ 17 | { 18 | loader: 'ts-loader', 19 | options: { 20 | transpileOnly: true, 21 | experimentalWatchApi: true, 22 | }, 23 | }, 24 | ], 25 | }, 26 | { 27 | test: /\.(svg|ttf|eot|woff|woff2)$/, 28 | type: 'asset/resource', 29 | // only process modules with this loader 30 | // if they live under a 'fonts' or 'pficon' directory 31 | include: [ 32 | path.resolve(__dirname, 'node_modules/patternfly/dist/fonts'), 33 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'), 34 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'), 35 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/fonts'), 36 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/pficon'), 37 | ], 38 | }, 39 | { 40 | test: /\.svg$/, 41 | type: 'asset/inline', 42 | include: (input) => input.indexOf('background-filter.svg') > 1, 43 | use: [ 44 | { 45 | options: { 46 | limit: 5000, 47 | outputPath: 'svgs', 48 | name: '[name].[ext]', 49 | }, 50 | }, 51 | ], 52 | }, 53 | { 54 | test: /\.svg$/, 55 | // only process SVG modules with this loader if they live under a 'bgimages' directory 56 | // this is primarily useful when applying a CSS background using an SVG 57 | include: (input) => input.indexOf(BG_IMAGES_DIRNAME) > -1, 58 | type: 'asset/inline', 59 | }, 60 | { 61 | test: /\.svg$/, 62 | // only process SVG modules with this loader when they don't live under a 'bgimages', 63 | // 'fonts', or 'pficon' directory, those are handled with other loaders 64 | include: (input) => 65 | input.indexOf(BG_IMAGES_DIRNAME) === -1 && 66 | input.indexOf('fonts') === -1 && 67 | input.indexOf('background-filter') === -1 && 68 | input.indexOf('pficon') === -1, 69 | use: { 70 | loader: 'raw-loader', 71 | options: {}, 72 | }, 73 | }, 74 | { 75 | test: /\.(jpg|jpeg|png|gif)$/i, 76 | include: [ 77 | path.resolve(__dirname, 'src'), 78 | path.resolve(__dirname, 'node_modules/patternfly'), 79 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/images'), 80 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css/assets/images'), 81 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/images'), 82 | path.resolve( 83 | __dirname, 84 | 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images' 85 | ), 86 | path.resolve( 87 | __dirname, 88 | 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images' 89 | ), 90 | path.resolve( 91 | __dirname, 92 | 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images' 93 | ), 94 | ], 95 | type: 'asset/inline', 96 | use: [ 97 | { 98 | options: { 99 | limit: 5000, 100 | outputPath: 'images', 101 | name: '[name].[ext]', 102 | }, 103 | }, 104 | ], 105 | }, 106 | ], 107 | }, 108 | output: { 109 | filename: '[name].bundle.js', 110 | path: path.resolve(__dirname, 'dist'), 111 | publicPath: ASSET_PATH, 112 | }, 113 | plugins: [ 114 | new HtmlWebpackPlugin({ 115 | template: path.resolve(__dirname, 'src', 'index.html'), 116 | }), 117 | new Dotenv({ 118 | systemvars: true, 119 | silent: true, 120 | }), 121 | new CopyPlugin({ 122 | patterns: [{ from: './src/favicon.png', to: 'images' }], 123 | }), 124 | ], 125 | resolve: { 126 | extensions: ['.js', '.ts', '.tsx', '.jsx'], 127 | plugins: [ 128 | new TsconfigPathsPlugin({ 129 | configFile: path.resolve(__dirname, './tsconfig.json'), 130 | }), 131 | ], 132 | symlinks: false, 133 | cacheWithContext: false, 134 | }, 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require('path'); 4 | const { merge } = require('webpack-merge'); 5 | const common = require('./webpack.common.js'); 6 | const { stylePaths } = require('./stylePaths'); 7 | const HOST = process.env.HOST || 'localhost'; 8 | const PORT = process.env.PORT || '9000'; 9 | 10 | module.exports = merge(common('development'), { 11 | mode: 'development', 12 | devtool: 'eval-source-map', 13 | devServer: { 14 | host: HOST, 15 | port: PORT, 16 | historyApiFallback: true, 17 | open: true, 18 | static: { 19 | directory: path.resolve(__dirname, 'dist'), 20 | }, 21 | client: { 22 | overlay: true, 23 | }, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.css$/, 29 | include: [...stylePaths], 30 | use: ['style-loader', 'css-loader'], 31 | }, 32 | ], 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const { merge } = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | const { stylePaths } = require('./stylePaths'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 8 | const TerserJSPlugin = require('terser-webpack-plugin'); 9 | 10 | module.exports = merge(common('production'), { 11 | mode: 'production', 12 | devtool: 'source-map', 13 | optimization: { 14 | minimizer: [ 15 | new TerserJSPlugin({}), 16 | new CssMinimizerPlugin({ 17 | minimizerOptions: { 18 | preset: ['default', { mergeLonghand: false }], 19 | }, 20 | }), 21 | ], 22 | }, 23 | plugins: [ 24 | new MiniCssExtractPlugin({ 25 | filename: '[name].css', 26 | chunkFilename: '[name].bundle.css', 27 | }), 28 | ], 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.css$/, 33 | include: [...stylePaths], 34 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 35 | }, 36 | ], 37 | }, 38 | }); 39 | --------------------------------------------------------------------------------