feat(ws): initial commit for frontend (#19)
* feat: client ui frontend scaffolding In this PR: - UI frontend scaffolding - Github Action for frontend and backend Most of the content of this PR is extract from https://github.com/patternfly/patternfly-react-seed/tree/v6. Thank you so much patternfly team for the seed! Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com> * Changes requested by code review Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com> * Fixing icons Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com> --------- Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com>
This commit is contained in:
parent
3e89605c4d
commit
b0367e8b3d
|
|
@ -0,0 +1,35 @@
|
||||||
|
name: UI - BFF - Test and Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "notebooks-v2", "v*-branch" ]
|
||||||
|
pull_request:
|
||||||
|
paths: [ "workspaces/backend/**" ]
|
||||||
|
branches: [ "main", "notebooks-v2", "v*-branch" ]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: workspaces/backend/go.mod
|
||||||
|
check-latest: true
|
||||||
|
cache-dependency-path: workspaces/backend/go.sum
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: workspaces/backend
|
||||||
|
run: make build
|
||||||
|
|
||||||
|
- name: Check if there are uncommitted file changes
|
||||||
|
working-directory: workspaces/backend
|
||||||
|
run: |
|
||||||
|
clean=$(git status --porcelain)
|
||||||
|
if [[ -z "$clean" ]]; then
|
||||||
|
echo "Empty git status --porcelain: $clean"
|
||||||
|
else
|
||||||
|
echo "Uncommitted file changes detected: $clean"
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: UI - Frontend - Test and Build
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main", "notebooks-v2", "v*-branch" ]
|
||||||
|
pull_request:
|
||||||
|
paths: [ "workspaces/frontend/**" ]
|
||||||
|
branches: [ "main", "notebooks-v2", "v*-branch" ]
|
||||||
|
jobs:
|
||||||
|
test-and-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: workspaces/frontend
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: workspaces/frontend
|
||||||
|
run: npm run test:coverage
|
||||||
|
|
||||||
|
- name: Clean
|
||||||
|
working-directory: workspaces/frontend
|
||||||
|
run: npm run clean
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
working-directory: workspaces/frontend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Check if there are uncommitted file changes
|
||||||
|
working-directory: workspaces/frontend
|
||||||
|
run: |
|
||||||
|
clean=$(git status --porcelain)
|
||||||
|
if [[ -z "$clean" ]]; then
|
||||||
|
echo "Empty git status --porcelain: $clean"
|
||||||
|
else
|
||||||
|
echo "Uncommitted file changes detected: $clean"
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# Kubeflow Workspaces Backend
|
# Kubeflow Workspaces Backend
|
||||||
The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the Kubeflow Workspaces UI as part of [Kubeflow Notebooks 2.0](https://github.com/kubeflow/kubeflow/issues/7156).
|
The Kubeflow Workspaces Backend is the _backend for frontend_ (BFF) used by the Kubeflow Workspaces Frontend as part of [Kubeflow Notebooks 2.0](https://github.com/kubeflow/kubeflow/issues/7156).
|
||||||
|
|
||||||
> ⚠️ __Warning__ ⚠️
|
> ⚠️ __Warning__ ⚠️
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
**/node_modules
|
||||||
|
dist
|
||||||
|
yarn-error.log
|
||||||
|
yarn.lock
|
||||||
|
stats.json
|
||||||
|
coverage
|
||||||
|
.idea
|
||||||
|
.env
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Kubeflow Workspaces Frontend
|
||||||
|
The Kubeflow Workspaces Frontend is the web user interface used to monitor and manage Kubeflow Workspaces as part of [Kubeflow Notebooks 2.0](https://github.com/kubeflow/kubeflow/issues/7156).
|
||||||
|
|
||||||
|
> ⚠️ __Warning__ ⚠️
|
||||||
|
>
|
||||||
|
> The Kubeflow Workspaces Frontend is a work in progress and is __NOT__ currently ready for use.
|
||||||
|
> We greatly appreciate any contributions.
|
||||||
|
|
||||||
|
|
||||||
|
## Pre-requisites:
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Install development/build dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# Run a production build (outputs to "dist" dir)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Run the test suite
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# Run the test suite with coverage
|
||||||
|
npm run test:coverage
|
||||||
|
|
||||||
|
# Run the linter
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Run the code formatter
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
# Launch a tool to inspect the bundle size
|
||||||
|
npm run bundle-profile:analyze
|
||||||
|
|
||||||
|
# Start the express server (run a production build first)
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = {};
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// For a detailed explanation regarding each configuration property, visit:
|
||||||
|
// https://jestjs.io/docs/en/configuration.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
// Automatically clear mock calls and instances between every test
|
||||||
|
clearMocks: true,
|
||||||
|
|
||||||
|
// Indicates whether the coverage information should be collected while executing the test
|
||||||
|
collectCoverage: false,
|
||||||
|
|
||||||
|
// The directory where Jest should output its coverage files
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
|
||||||
|
// An array of directory names to be searched recursively up from the requiring module's location
|
||||||
|
moduleDirectories: [
|
||||||
|
'node_modules',
|
||||||
|
'<rootDir>/src'
|
||||||
|
],
|
||||||
|
|
||||||
|
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||||
|
moduleNameMapper: {
|
||||||
|
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||||
|
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
|
||||||
|
'@app/(.*)': '<rootDir>/src/app/$1'
|
||||||
|
},
|
||||||
|
|
||||||
|
// A preset that is used as a base for Jest's configuration
|
||||||
|
preset: 'ts-jest/presets/js-with-ts',
|
||||||
|
|
||||||
|
// The test environment that will be used for testing.
|
||||||
|
testEnvironment: 'jsdom'
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,80 @@
|
||||||
|
{
|
||||||
|
"name": "kubeflow-workspaces-frontend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "The Kubeflow Workspaces Frontend is the web user interface used to monitor and manage Kubeflow Workspaces as part of Kubeflow Notebooks 2.0",
|
||||||
|
"repository": "https://github.com/kubeflow/notebooks.git",
|
||||||
|
"homepage": "https://github.com/kubeflow/notebooks",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"prebuild": "npm run type-check && npm run clean",
|
||||||
|
"build": "webpack --config webpack.prod.js",
|
||||||
|
"start": "sirv dist --cors --single --host --port 8080",
|
||||||
|
"start:dev": "webpack serve --color --progress --config webpack.dev.js",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:coverage": "jest --coverage",
|
||||||
|
"eslint": "eslint --ext .tsx,.js ./src/",
|
||||||
|
"lint": "npm run eslint",
|
||||||
|
"format": "prettier --check --write ./src/**/*.{tsx,ts}",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"ci-checks": "npm run type-check && npm run lint && npm run test:coverage",
|
||||||
|
"build:bundle-profile": "webpack --config webpack.prod.js --profile --json > stats.json",
|
||||||
|
"bundle-profile:analyze": "npm run build:bundle-profile && webpack-bundle-analyzer ./stats.json",
|
||||||
|
"clean": "rimraf dist"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
|
"@testing-library/react": "^14.0.0",
|
||||||
|
"@testing-library/user-event": "14.4.3",
|
||||||
|
"@types/jest": "^29.5.3",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@types/victory": "^33.1.5",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
|
"css-loader": "^6.11.0",
|
||||||
|
"css-minimizer-webpack-plugin": "^5.0.1",
|
||||||
|
"dotenv-webpack": "^8.0.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"eslint-plugin-react": "^7.34.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"html-webpack-plugin": "^5.6.0",
|
||||||
|
"imagemin": "^8.0.1",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"mini-css-extract-plugin": "^2.9.0",
|
||||||
|
"postcss": "^8.4.38",
|
||||||
|
"prettier": "^3.3.0",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"react-axe": "^3.5.4",
|
||||||
|
"react-docgen-typescript-loader": "^3.7.2",
|
||||||
|
"react-router-dom": "^5.3.4",
|
||||||
|
"regenerator-runtime": "^0.13.11",
|
||||||
|
"rimraf": "^5.0.7",
|
||||||
|
"style-loader": "^3.3.4",
|
||||||
|
"svg-url-loader": "^8.0.0",
|
||||||
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
|
"ts-jest": "^29.1.4",
|
||||||
|
"ts-loader": "^9.5.1",
|
||||||
|
"tsconfig-paths-webpack-plugin": "^4.1.0",
|
||||||
|
"tslib": "^2.6.0",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.91.0",
|
||||||
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"webpack-cli": "^5.1.4",
|
||||||
|
"webpack-dev-server": "^4.15.2",
|
||||||
|
"webpack-merge": "^5.10.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@patternfly/react-core": "6.0.0-alpha.68",
|
||||||
|
"@patternfly/react-icons": "6.0.0-alpha.24",
|
||||||
|
"@patternfly/react-styles": "6.0.0-alpha.24",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"sirv-cli": "^2.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
Masthead,
|
||||||
|
MastheadContent,
|
||||||
|
MastheadToggle,
|
||||||
|
Nav,
|
||||||
|
NavExpandable,
|
||||||
|
NavItem,
|
||||||
|
NavList,
|
||||||
|
Page,
|
||||||
|
PageSidebar,
|
||||||
|
PageSidebarBody,
|
||||||
|
SkipToContent,
|
||||||
|
Title
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { IAppRoute, IAppRouteGroup, routes } from '@app/routes';
|
||||||
|
import { BarsIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
interface IAppLayout {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppLayout: React.FunctionComponent<IAppLayout> = ({ children }) => {
|
||||||
|
const [sidebarOpen, setSidebarOpen] = React.useState(true);
|
||||||
|
const masthead = (
|
||||||
|
<Masthead>
|
||||||
|
<MastheadToggle>
|
||||||
|
<Button variant="plain" onClick={() => setSidebarOpen(!sidebarOpen)} aria-label="Global navigation">
|
||||||
|
<BarsIcon />
|
||||||
|
</Button>
|
||||||
|
</MastheadToggle>
|
||||||
|
|
||||||
|
<MastheadContent>
|
||||||
|
<Flex>
|
||||||
|
<Title headingLevel="h2" size="3xl">
|
||||||
|
Kubeflow Workspaces
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
</MastheadContent>
|
||||||
|
</Masthead>
|
||||||
|
);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const renderNavItem = (route: IAppRoute, index: number) => (
|
||||||
|
<NavItem key={`${route.label}-${index}`} id={`${route.label}-${index}`} isActive={route.path === location.pathname}>
|
||||||
|
<NavLink exact={route.exact} to={route.path}>
|
||||||
|
{route.label}
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNavGroup = (group: IAppRouteGroup, groupIndex: number) => (
|
||||||
|
<NavExpandable
|
||||||
|
key={`${group.label}-${groupIndex}`}
|
||||||
|
id={`${group.label}-${groupIndex}`}
|
||||||
|
title={group.label}
|
||||||
|
isActive={group.routes.some((route) => route.path === location.pathname)}
|
||||||
|
>
|
||||||
|
{group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
|
||||||
|
</NavExpandable>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Navigation = (
|
||||||
|
<Nav id="nav-primary-simple">
|
||||||
|
<NavList id="nav-list-simple">
|
||||||
|
{routes.map(
|
||||||
|
(route, idx) => route.label && (!route.routes ? renderNavItem(route, idx) : renderNavGroup(route, idx))
|
||||||
|
)}
|
||||||
|
</NavList>
|
||||||
|
</Nav>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Sidebar = (
|
||||||
|
<PageSidebar>
|
||||||
|
<PageSidebarBody>
|
||||||
|
{Navigation}
|
||||||
|
</PageSidebarBody>
|
||||||
|
</PageSidebar>
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageId = 'primary-app-container';
|
||||||
|
|
||||||
|
const PageSkipToContent = (
|
||||||
|
<SkipToContent onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const primaryContentContainer = document.getElementById(pageId);
|
||||||
|
primaryContentContainer && primaryContentContainer.focus();
|
||||||
|
}} href={`#${pageId}`}>
|
||||||
|
Skip to Content
|
||||||
|
</SkipToContent>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
mainContainerId={pageId}
|
||||||
|
masthead={masthead}
|
||||||
|
sidebar={sidebarOpen && Sidebar}
|
||||||
|
skipToContent={PageSkipToContent}>
|
||||||
|
{children}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { AppLayout };
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateBody,
|
||||||
|
EmptyStateFooter,
|
||||||
|
EmptyStateVariant,
|
||||||
|
PageSection,
|
||||||
|
Text,
|
||||||
|
TextContent
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
|
||||||
|
const Dashboard: React.FunctionComponent = () => (
|
||||||
|
<PageSection>
|
||||||
|
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Dashboard Module)" icon={CubesIcon}>
|
||||||
|
<EmptyStateBody>
|
||||||
|
<TextContent>
|
||||||
|
<Text component="p">
|
||||||
|
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple enough to use but
|
||||||
|
flexible enough to meet a variety of needs.
|
||||||
|
</Text>
|
||||||
|
</TextContent>
|
||||||
|
</EmptyStateBody><EmptyStateFooter>
|
||||||
|
<Button variant="primary">Primary Action</Button>
|
||||||
|
|
||||||
|
</EmptyStateFooter></EmptyState>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Dashboard };
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||||
|
import { Button, EmptyState, EmptyStateBody, EmptyStateFooter, PageSection } from '@patternfly/react-core';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
const NotFound: React.FunctionComponent = () => {
|
||||||
|
function GoHomeBtn() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
history.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button onClick={handleClick}>Take me home</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<EmptyState titleText="404 Page not found" variant="full" icon={ExclamationTriangleIcon}>
|
||||||
|
<EmptyStateBody>
|
||||||
|
We didn't find a page that matches the address you navigated to.
|
||||||
|
</EmptyStateBody><EmptyStateFooter>
|
||||||
|
<GoHomeBtn />
|
||||||
|
</EmptyStateFooter></EmptyState>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { NotFound };
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { CubesIcon } from '@patternfly/react-icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
EmptyState,
|
||||||
|
EmptyStateBody,
|
||||||
|
EmptyStateFooter,
|
||||||
|
EmptyStateVariant,
|
||||||
|
PageSection,
|
||||||
|
Text,
|
||||||
|
TextContent
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
|
||||||
|
const Support: React.FunctionComponent = () => (
|
||||||
|
<PageSection>
|
||||||
|
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Stub Support Module)" icon={CubesIcon}>
|
||||||
|
<EmptyStateBody>
|
||||||
|
<TextContent>
|
||||||
|
<Text component="p">
|
||||||
|
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple enough to use but
|
||||||
|
flexible enough to meet a variety of needs.
|
||||||
|
</Text>
|
||||||
|
</TextContent>
|
||||||
|
</EmptyStateBody><EmptyStateFooter>
|
||||||
|
<Button variant="primary">Primary Action</Button>
|
||||||
|
|
||||||
|
</EmptyStateFooter></EmptyState>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { Support };
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`App tests should render default App component 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-page"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-skip-to-content"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-disabled="false"
|
||||||
|
class="pf-v6-c-button pf-m-primary"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Button-primary-1"
|
||||||
|
data-ouia-component-type="PF6/Button"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
href="#primary-app-container"
|
||||||
|
>
|
||||||
|
Skip to Content
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<header
|
||||||
|
class="pf-v6-c-masthead pf-m-display-inline-on-md"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pf-v6-c-masthead__toggle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
aria-label="Global navigation"
|
||||||
|
class="pf-v6-c-button pf-m-plain"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Button-plain-1"
|
||||||
|
data-ouia-component-type="PF6/Button"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pf-v6-svg"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M16 132h416c8.837 0 16-7.163 16-16V76c0-8.837-7.163-16-16-16H16C7.163 60 0 67.163 0 76v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16zm0 160h416c8.837 0 16-7.163 16-16v-40c0-8.837-7.163-16-16-16H16c-8.837 0-16 7.163-16 16v40c0 8.837 7.163 16 16 16z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-masthead__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-l-flex"
|
||||||
|
>
|
||||||
|
<h2
|
||||||
|
class="pf-v6-c-title pf-m-3xl"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Title-1"
|
||||||
|
data-ouia-component-type="PF6/Title"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
>
|
||||||
|
Kubeflow Workspaces
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
aria-hidden="false"
|
||||||
|
class="pf-v6-c-page__sidebar pf-m-expanded"
|
||||||
|
id="page-sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-page__sidebar-body"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
aria-label="Global"
|
||||||
|
class="pf-v6-c-nav"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Nav-1"
|
||||||
|
data-ouia-component-type="PF6/Nav"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
id="nav-primary-simple"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="pf-v6-c-nav__list"
|
||||||
|
id="nav-list-simple"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="pf-v6-c-nav__item"
|
||||||
|
data-ouia-component-id="OUIA-Generated-NavItem-1"
|
||||||
|
data-ouia-component-type="PF6/NavItem"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
aria-current="page"
|
||||||
|
class="pf-v6-c-nav__link pf-m-current active"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
class="pf-v6-c-nav__item"
|
||||||
|
data-ouia-component-id="OUIA-Generated-NavItem-2"
|
||||||
|
data-ouia-component-type="PF6/NavItem"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="pf-v6-c-nav__link"
|
||||||
|
href="/support"
|
||||||
|
>
|
||||||
|
Support
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-page__main-container"
|
||||||
|
>
|
||||||
|
<main
|
||||||
|
class="pf-v6-c-page__main"
|
||||||
|
id="primary-app-container"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<section
|
||||||
|
class="pf-v6-c-page__main-section"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__content"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__icon"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="pf-v6-svg"
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M488.6 250.2L392 214V105.5c0-15-9.3-28.4-23.4-33.7l-100-37.5c-8.1-3.1-17.1-3.1-25.3 0l-100 37.5c-14.1 5.3-23.4 18.7-23.4 33.7V214l-96.6 36.2C9.3 255.5 0 268.9 0 283.9V394c0 13.6 7.7 26.1 19.9 32.2l100 50c10.1 5.1 22.1 5.1 32.2 0l103.9-52 103.9 52c10.1 5.1 22.1 5.1 32.2 0l100-50c12.2-6.1 19.9-18.6 19.9-32.2V283.9c0-15-9.3-28.4-23.4-33.7zM358 214.8l-85 31.9v-68.2l85-37v73.3zM154 104.1l102-38.2 102 38.2v.6l-102 41.4-102-41.4v-.6zm84 291.1l-85 42.5v-79.1l85-38.8v75.4zm0-112l-102 41.4-102-41.4v-.6l102-38.2 102 38.2v.6zm240 112l-85 42.5v-79.1l85-38.8v75.4zm0-112l-102 41.4-102-41.4v-.6l102-38.2 102 38.2v.6z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__title"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
class="pf-v6-c-empty-state__title-text"
|
||||||
|
>
|
||||||
|
Empty State (Dashboard Module)
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-content"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="pf-v6-c-content--p"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Text-1"
|
||||||
|
data-ouia-component-type="PF6/Text"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
data-pf-content="true"
|
||||||
|
>
|
||||||
|
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple enough to use but flexible enough to meet a variety of needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pf-v6-c-empty-state__footer"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
aria-disabled="false"
|
||||||
|
class="pf-v6-c-button pf-m-primary"
|
||||||
|
data-ouia-component-id="OUIA-Generated-Button-primary-2"
|
||||||
|
data-ouia-component-type="PF6/Button"
|
||||||
|
data-ouia-safe="true"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Primary Action
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import App from "@app/index";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import "@testing-library/jest-dom";
|
||||||
|
|
||||||
|
describe("App tests", () => {
|
||||||
|
test("should render default App component", () => {
|
||||||
|
const { asFragment } = render(<App />);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render a nav-toggle button", () => {
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole("button", { name: "Global navigation" }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import "@patternfly/react-core/dist/styles/base.css";
|
||||||
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
|
import { AppLayout } from "@app/AppLayout/AppLayout";
|
||||||
|
import { AppRoutes } from "@app/routes";
|
||||||
|
import "@app/app.css";
|
||||||
|
|
||||||
|
const App: React.FunctionComponent = () => (
|
||||||
|
<Router>
|
||||||
|
<AppLayout>
|
||||||
|
<AppRoutes />
|
||||||
|
</AppLayout>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
Route,
|
||||||
|
RouteComponentProps,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
} from "react-router-dom";
|
||||||
|
import { Dashboard } from "@app/Dashboard/Dashboard";
|
||||||
|
import { Support } from "@app/Support/Support";
|
||||||
|
import { NotFound } from "@app/NotFound/NotFound";
|
||||||
|
import { useDocumentTitle } from "@app/utils/useDocumentTitle";
|
||||||
|
|
||||||
|
let routeFocusTimer: number;
|
||||||
|
|
||||||
|
export interface IAppRoute {
|
||||||
|
label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
component:
|
||||||
|
| React.ComponentType<RouteComponentProps<any>>
|
||||||
|
| React.ComponentType<any>;
|
||||||
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||||
|
exact?: boolean;
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
routes?: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAppRouteGroup {
|
||||||
|
label: string;
|
||||||
|
routes: IAppRoute[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppRouteConfig = IAppRoute | IAppRouteGroup;
|
||||||
|
|
||||||
|
const routes: AppRouteConfig[] = [
|
||||||
|
{
|
||||||
|
component: Dashboard,
|
||||||
|
exact: true,
|
||||||
|
label: "Dashboard",
|
||||||
|
path: "/",
|
||||||
|
title: "Kubeflow Workspaces | Main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
component: Support,
|
||||||
|
exact: true,
|
||||||
|
label: "Support",
|
||||||
|
path: "/support",
|
||||||
|
title: "Kubeflow Workspaces | Support Page",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// a custom hook for sending focus to the primary content container
|
||||||
|
// after a view has loaded so that subsequent press of tab key
|
||||||
|
// sends focus directly to relevant content
|
||||||
|
// may not be necessary if https://github.com/ReactTraining/react-router/issues/5210 is resolved
|
||||||
|
const useA11yRouteChange = () => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
React.useEffect(() => {
|
||||||
|
routeFocusTimer = window.setTimeout(() => {
|
||||||
|
const mainContainer = document.getElementById("primary-app-container");
|
||||||
|
if (mainContainer) {
|
||||||
|
mainContainer.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(routeFocusTimer);
|
||||||
|
};
|
||||||
|
}, [pathname]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RouteWithTitleUpdates = ({
|
||||||
|
component: Component,
|
||||||
|
title,
|
||||||
|
...rest
|
||||||
|
}: IAppRoute) => {
|
||||||
|
useA11yRouteChange();
|
||||||
|
useDocumentTitle(title);
|
||||||
|
|
||||||
|
function routeWithTitle(routeProps: RouteComponentProps) {
|
||||||
|
return <Component {...rest} {...routeProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Route render={routeWithTitle} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PageNotFound = ({ title }: { title: string }) => {
|
||||||
|
useDocumentTitle(title);
|
||||||
|
return <Route component={NotFound} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedRoutes: IAppRoute[] = routes.reduce(
|
||||||
|
(flattened, route) => [
|
||||||
|
...flattened,
|
||||||
|
...(route.routes ? route.routes : [route]),
|
||||||
|
],
|
||||||
|
[] as IAppRoute[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const AppRoutes = (): React.ReactElement => (
|
||||||
|
<Switch>
|
||||||
|
{flattenedRoutes.map(({ path, exact, component, title }, idx) => (
|
||||||
|
<RouteWithTitleUpdates
|
||||||
|
path={path}
|
||||||
|
exact={exact}
|
||||||
|
component={component}
|
||||||
|
key={idx}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<PageNotFound title="404 Page Not Found" />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
|
||||||
|
export { AppRoutes, routes };
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
// a custom hook for setting the page title
|
||||||
|
export function useDocumentTitle(title: string) {
|
||||||
|
React.useEffect(() => {
|
||||||
|
const originalTitle = document.title;
|
||||||
|
document.title = title;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.title = originalTitle;
|
||||||
|
};
|
||||||
|
}, [title]);
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-US">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Kubeflow Workspaces</title>
|
||||||
|
<meta content="Kubeflow Workspaces" id="appName" name="application-name">
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
|
<link href="/images/favicon.ico" rel="icon" type="image/x-icon">
|
||||||
|
<base href="/">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>Enabling JavaScript is required to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from '@app/index';
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const config = {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
id: 'color-contrast',
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef
|
||||||
|
const axe = require('react-axe');
|
||||||
|
axe(React, ReactDOM, 1000, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root') as Element);
|
||||||
|
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
declare module "*.png";
|
||||||
|
declare module "*.jpg";
|
||||||
|
declare module "*.jpeg";
|
||||||
|
declare module "*.gif";
|
||||||
|
declare module "*.svg";
|
||||||
|
declare module "*.css";
|
||||||
|
declare module "*.wav";
|
||||||
|
declare module "*.mp3";
|
||||||
|
declare module "*.m4a";
|
||||||
|
declare module "*.rdf";
|
||||||
|
declare module "*.ttl";
|
||||||
|
declare module "*.pdf";
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
const path = require('path');
|
||||||
|
module.exports = {
|
||||||
|
stylePaths: [
|
||||||
|
path.resolve(__dirname, 'src'),
|
||||||
|
path.resolve(__dirname, 'node_modules/patternfly'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/patternfly'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css')
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"module": "esnext",
|
||||||
|
"target": "ES6",
|
||||||
|
"lib": [
|
||||||
|
"es6",
|
||||||
|
"dom"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"allowJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"paths": {
|
||||||
|
"@app/*": [
|
||||||
|
"src/app/*"
|
||||||
|
],
|
||||||
|
"@assets/*": [
|
||||||
|
"node_modules/@patternfly/react-core/dist/styles/assets/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"importHelpers": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.jsx",
|
||||||
|
"**/*.js",
|
||||||
|
".eslintrc.js"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CopyPlugin = require('copy-webpack-plugin');
|
||||||
|
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||||
|
const Dotenv = require('dotenv-webpack');
|
||||||
|
const BG_IMAGES_DIRNAME = 'bgimages';
|
||||||
|
const ASSET_PATH = process.env.ASSET_PATH || '/';
|
||||||
|
module.exports = (env) => {
|
||||||
|
return {
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(tsx|ts|jsx)?$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
experimentalWatchApi: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(svg|ttf|eot|woff|woff2)$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
// only process modules with this loader
|
||||||
|
// if they live under a 'fonts' or 'pficon' directory
|
||||||
|
include: [
|
||||||
|
path.resolve(__dirname, 'node_modules/patternfly/dist/fonts'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/fonts'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/pficon')
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
type: 'asset/inline',
|
||||||
|
include: (input) => input.indexOf('background-filter.svg') > 1,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
limit: 5000,
|
||||||
|
outputPath: 'svgs',
|
||||||
|
name: '[name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
// only process SVG modules with this loader if they live under a 'bgimages' directory
|
||||||
|
// this is primarily useful when applying a CSS background using an SVG
|
||||||
|
include: (input) => input.indexOf(BG_IMAGES_DIRNAME) > -1,
|
||||||
|
type: 'asset/inline'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.svg$/,
|
||||||
|
// only process SVG modules with this loader when they don't live under a 'bgimages',
|
||||||
|
// 'fonts', or 'pficon' directory, those are handled with other loaders
|
||||||
|
include: (input) =>
|
||||||
|
input.indexOf(BG_IMAGES_DIRNAME) === -1 &&
|
||||||
|
input.indexOf('fonts') === -1 &&
|
||||||
|
input.indexOf('background-filter') === -1 &&
|
||||||
|
input.indexOf('pficon') === -1,
|
||||||
|
use: {
|
||||||
|
loader: 'raw-loader',
|
||||||
|
options: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(jpg|jpeg|png|gif)$/i,
|
||||||
|
include: [
|
||||||
|
path.resolve(__dirname, 'src'),
|
||||||
|
path.resolve(__dirname, 'node_modules/patternfly'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/images'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css/assets/images'),
|
||||||
|
path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/images'),
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images'
|
||||||
|
),
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images'
|
||||||
|
),
|
||||||
|
path.resolve(
|
||||||
|
__dirname,
|
||||||
|
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images'
|
||||||
|
)
|
||||||
|
],
|
||||||
|
type: 'asset/inline',
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
limit: 5000,
|
||||||
|
outputPath: 'images',
|
||||||
|
name: '[name].[ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: '[name].bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
publicPath: ASSET_PATH
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: path.resolve(__dirname, 'src', 'index.html')
|
||||||
|
}),
|
||||||
|
new Dotenv({
|
||||||
|
systemvars: true,
|
||||||
|
silent: true
|
||||||
|
}),
|
||||||
|
new CopyPlugin({
|
||||||
|
patterns: [{ from: './src/favicon.ico', to: 'images' }]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.tsx', '.jsx'],
|
||||||
|
plugins: [
|
||||||
|
new TsconfigPathsPlugin({
|
||||||
|
configFile: path.resolve(__dirname, './tsconfig.json')
|
||||||
|
})
|
||||||
|
],
|
||||||
|
symlinks: false,
|
||||||
|
cacheWithContext: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.common.js');
|
||||||
|
const { stylePaths } = require('./stylePaths');
|
||||||
|
const HOST = process.env.HOST || 'localhost';
|
||||||
|
const PORT = process.env.PORT || '9000';
|
||||||
|
|
||||||
|
module.exports = merge(common('development'), {
|
||||||
|
mode: 'development',
|
||||||
|
devtool: 'eval-source-map',
|
||||||
|
devServer: {
|
||||||
|
host: HOST,
|
||||||
|
port: PORT,
|
||||||
|
historyApiFallback: true,
|
||||||
|
open: true,
|
||||||
|
static: {
|
||||||
|
directory: path.resolve(__dirname, 'dist')
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
overlay: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
include: [...stylePaths],
|
||||||
|
use: ['style-loader', 'css-loader']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
|
const { merge } = require('webpack-merge');
|
||||||
|
const common = require('./webpack.common.js');
|
||||||
|
const { stylePaths } = require('./stylePaths');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||||
|
const TerserJSPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
|
module.exports = merge(common('production'), {
|
||||||
|
mode: 'production',
|
||||||
|
devtool: 'source-map',
|
||||||
|
optimization: {
|
||||||
|
minimizer: [
|
||||||
|
new TerserJSPlugin({}),
|
||||||
|
new CssMinimizerPlugin({
|
||||||
|
minimizerOptions: {
|
||||||
|
preset: ['default', { mergeLonghand: false }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: '[name].css',
|
||||||
|
chunkFilename: '[name].bundle.css'
|
||||||
|
})
|
||||||
|
],
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
include: [...stylePaths],
|
||||||
|
use: [MiniCssExtractPlugin.loader, 'css-loader']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue