chore(ws): restructure frontend and setup navigation (#67)

Signed-off-by: Griffin-Sullivan <gsulliva@redhat.com>
This commit is contained in:
Griffin Sullivan 2024-10-23 14:52:30 -04:00 committed by GitHub
parent e46633be34
commit a3a7c84b95
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 833 additions and 957 deletions

View File

@ -20,13 +20,9 @@ jobs:
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
run: npm run build:clean
- name: Build
working-directory: workspaces/frontend

View File

@ -9,7 +9,7 @@ The Kubeflow Workspaces Frontend is the web user interface used to monitor and m
## Pre-requisites:
TBD
Node v20
## Development
@ -23,20 +23,8 @@ 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
# Run the unit test suite
npm run test:jest
# Start the express server (run a production build first)
npm run start

View File

@ -0,0 +1,15 @@
const path = require('path');
const relativeDir = path.resolve(__dirname, '..');
module.exports = {
stylePaths: [
path.resolve(relativeDir, 'src'),
path.resolve(relativeDir, 'node_modules/patternfly'),
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-styles/css'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/base.css'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css')
]
};

View File

@ -5,8 +5,9 @@ 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 || '/';
const IMAGES_DIRNAME = 'images';
const relativeDir = path.resolve(__dirname, '..');
module.exports = (env) => {
return {
module: {
@ -29,66 +30,80 @@ module.exports = (env) => {
// 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')
]
path.resolve(relativeDir, 'node_modules/patternfly/dist/fonts'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'),
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly/assets/fonts'),
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly/assets/pficon')
],
use: {
loader: 'file-loader',
options: {
// Limit at 50k. larger files emitted into separate files
limit: 5000,
outputPath: 'fonts',
name: '[name].[ext]',
},
},
},
{
test: /\.svg$/,
type: 'asset/inline',
include: (input) => input.indexOf('background-filter.svg') > 1,
use: [
{
loader: 'url-loader',
options: {
limit: 5000,
outputPath: 'svgs',
name: '[name].[ext]'
}
}
]
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'
include: (input) => input.indexOf(IMAGES_DIRNAME) > -1,
use: {
loader: 'svg-url-loader',
options: {
limit: 10000,
},
},
},
{
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(IMAGES_DIRNAME) === -1 &&
input.indexOf('fonts') === -1 &&
input.indexOf('background-filter') === -1 &&
input.indexOf('pficon') === -1,
use: {
loader: 'raw-loader',
options: {}
}
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(relativeDir, 'src'),
path.resolve(relativeDir, 'node_modules/patternfly'),
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly/assets/images'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-styles/css/assets/images'),
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/assets/images'),
path.resolve(
__dirname,
relativeDir,
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images'
),
path.resolve(
__dirname,
relativeDir,
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images'
),
path.resolve(
__dirname,
relativeDir,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images'
)
],
@ -102,35 +117,46 @@ module.exports = (env) => {
}
}
]
},
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
'style-loader',
// Translates CSS into CommonJS
'css-loader',
// Compiles Sass to CSS
'sass-loader',
],
}
]
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
path: path.resolve(relativeDir, 'dist'),
publicPath: ASSET_PATH
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src', 'index.html')
template: path.resolve(relativeDir, 'src', 'index.html')
}),
new Dotenv({
systemvars: true,
silent: true
}),
new CopyPlugin({
patterns: [{ from: './src/favicon.ico', to: 'images' }]
patterns: [{ from: './src/images', to: 'images' }]
})
],
resolve: {
extensions: ['.js', '.ts', '.tsx', '.jsx'],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, './tsconfig.json')
configFile: path.resolve(relativeDir, './tsconfig.json')
})
],
symlinks: false,
cacheWithContext: false
}
};
};
};

View File

@ -6,6 +6,10 @@ const common = require('./webpack.common.js');
const { stylePaths } = require('./stylePaths');
const HOST = process.env.HOST || 'localhost';
const PORT = process.env.PORT || '9000';
const PROXY_HOST = process.env.PROXY_HOST || 'localhost';
const PROXY_PORT = process.env.PROXY_PORT || '4000';
const PROXY_PROTOCOL = process.env.PROXY_PROTOCOL || 'http:';
const relativeDir = path.resolve(__dirname, '..');
module.exports = merge(common('development'), {
mode: 'development',
@ -14,21 +18,31 @@ module.exports = merge(common('development'), {
host: HOST,
port: PORT,
historyApiFallback: true,
open: true,
static: {
directory: path.resolve(__dirname, 'dist')
directory: path.resolve(relativeDir, 'dist'),
},
client: {
overlay: true
}
overlay: true,
},
proxy: [
{
context: ["/api"],
target: {
host: PROXY_HOST,
protocol: PROXY_PROTOCOL,
port: PROXY_PORT,
},
changeOrigin: true,
},
],
},
module: {
rules: [
{
test: /\.css$/,
include: [...stylePaths],
use: ['style-loader', 'css-loader']
}
]
}
use: ['style-loader', 'css-loader'],
},
],
},
});

File diff suppressed because it is too large Load Diff

View File

@ -6,22 +6,18 @@
"homepage": "https://github.com/kubeflow/notebooks",
"license": "Apache-2.0",
"private": true,
"engines": {
"node": ">=20.0.0"
},
"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"
"build": "run-s build:prod",
"build:analyze": "run-s build build:bundle-profile build:bundle-analyze",
"build:bundle-profile": "webpack --config ./config/webpack.prod.js --profile --json > ./bundle.stats.json",
"build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json",
"build:clean": "rimraf ./dist",
"build:prod": "webpack --config ./config/webpack.prod.js",
"start:dev": "webpack serve --hot --color --config ./config/webpack.dev.js",
"test:jest": "jest"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.16.5",
@ -49,7 +45,7 @@
"prettier": "^3.3.0",
"prop-types": "^15.8.1",
"raw-loader": "^4.0.2",
"react-router-dom": "^5.3.4",
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.13.11",
"rimraf": "^5.0.7",
"style-loader": "^3.3.4",
@ -71,8 +67,10 @@
"@patternfly/react-core": "6.0.0-alpha.68",
"@patternfly/react-icons": "6.0.0-alpha.24",
"@patternfly/react-styles": "6.0.0-alpha.24",
"npm-run-all": "^4.1.5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6.26.2",
"sirv-cli": "^2.0.2"
}
}

View File

@ -0,0 +1,48 @@
import * as React from 'react';
import '@patternfly/react-core/dist/styles/base.css';
import AppRoutes from './AppRoutes';
import './app.css';
import {
Flex,
Masthead,
MastheadContent,
MastheadToggle,
Page,
PageToggleButton,
Title
} from '@patternfly/react-core';
import NavSidebar from './NavSidebar';
import { BarsIcon } from '@patternfly/react-icons';
const App: React.FC = () => {
const masthead = (
<Masthead>
<MastheadToggle>
<PageToggleButton id="page-nav-toggle" variant="plain" aria-label="Dashboard navigation">
<BarsIcon />
</PageToggleButton>
</MastheadToggle>
<MastheadContent>
<Flex>
<Title headingLevel="h2" size="3xl">
Kubeflow Notebooks 2.0
</Title>
</Flex>
</MastheadContent>
</Masthead>
);
return (
<Page
mainContainerId='primary-app-container'
masthead={masthead}
isManagedSidebar
sidebar={<NavSidebar />}
>
<AppRoutes />
</Page>
);
};
export default App;

View File

@ -1,107 +0,0 @@
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 };

View File

@ -0,0 +1,68 @@
import * as React from 'react';
import { Route, Routes } from 'react-router-dom';
import { NotFound } from './pages/notFound/NotFound';
import { Settings } from './pages/Settings/Settings';
import { Dashboard } from './pages/Dashboard/Dashboard';
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
'children' in navItem;
type NavDataCommon = {
label: string;
};
export type NavDataHref = NavDataCommon & {
path: string;
};
export type NavDataGroup = NavDataCommon & {
children: NavDataHref[];
};
type NavDataItem = NavDataHref | NavDataGroup;
export const useAdminSettings = (): NavDataItem[] => {
// get auth access for example set admin as true
const isAdmin = true; //this should be a call to getting auth / role access
// TODO: Remove the linter skip when we implement authentication
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!isAdmin) {
return [];
}
return [
{
label: 'Settings',
children: [{ label: 'Notebooks', path: '/notebookSettings' }],
},
];
};
export const useNavData = (): NavDataItem[] => [
{
label: 'Notebooks',
path: '/',
},
...useAdminSettings(),
];
const AppRoutes: React.FC = () => {
const isAdmin = true;
return (
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="*" element={<NotFound />} />
{
// TODO: Remove the linter skip when we implement authentication
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
isAdmin && (
<Route path="/notebookSettings/*" element={<Settings />} />
)
}
</Routes>
);
};
export default AppRoutes;

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import { NavLink } from 'react-router-dom';
import {
Nav,
NavExpandable,
NavItem,
NavList,
PageSidebar,
PageSidebarBody,
} from '@patternfly/react-core';
import { useNavData, isNavDataGroup, NavDataHref, NavDataGroup } from './AppRoutes';
const NavHref: React.FC<{ item: NavDataHref }> = ({ item }) => (
<NavItem key={item.label} data-id={item.label} itemId={item.label}>
<NavLink to={item.path}>{item.label}</NavLink>
</NavItem>
);
const NavGroup: React.FC<{ item: NavDataGroup }> = ({ item }) => {
const { children } = item;
const [expanded, setExpanded] = React.useState(false);
return (
<NavExpandable
data-id={item.label}
key={item.label}
id={item.label}
title={item.label}
groupId={item.label}
isExpanded={expanded}
onExpand={(e, val) => setExpanded(val)}
aria-label={item.label}
>
{children.map((childItem) => (
<NavHref key={childItem.label} data-id={childItem.label} item={childItem} />
))}
</NavExpandable>
);
};
const NavSidebar: React.FC = () => {
const navData = useNavData();
return (
<PageSidebar>
<PageSidebarBody>
<Nav id="nav-primary-simple">
<NavList id="nav-list-simple">
{navData.map((item) =>
isNavDataGroup(item) ? (
<NavGroup key={item.label} item={item} />
) : (
<NavHref key={item.label} item={item} />
),
)}
</NavList>
</Nav>
</PageSidebarBody>
</PageSidebar>
);
};
export default NavSidebar;

View File

@ -1,206 +0,0 @@
// 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>
`;

View File

@ -1,20 +0,0 @@
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();
});
});

View File

@ -1,16 +0,0 @@
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;

View File

@ -11,9 +11,9 @@ import {
TextContent
} from '@patternfly/react-core';
const Support: React.FunctionComponent = () => (
const Settings: React.FunctionComponent = () => (
<PageSection>
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Stub Support Module)" icon={CubesIcon}>
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Stub Settings Module)" icon={CubesIcon}>
<EmptyStateBody>
<TextContent>
<Text component="p">
@ -28,4 +28,4 @@ const Support: React.FunctionComponent = () => (
</PageSection>
);
export { Support };
export { Settings };

View File

@ -1,14 +1,14 @@
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';
import { useNavigate } from 'react-router-dom';
const NotFound: React.FunctionComponent = () => {
function GoHomeBtn() {
const history = useHistory();
const navigate = useNavigate();
function handleClick() {
history.push('/');
navigate('/');
}
return (

View File

@ -1,114 +0,0 @@
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 };

View File

@ -1,13 +0,0 @@
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.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="130px" height="32px" viewBox="0 0 130 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>Kubeflow Logo</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="0 0.24845283 42.930717 0.24845283 42.930717 15.9818868 0 15.9818868"></polygon>
<polygon id="path-3" points="0.19954717 0.267471698 15.8381887 0.267471698 15.8381887 15.9818868 0.19954717 15.9818868"></polygon>
</defs>
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="collapsed-nav/expanded" transform="translate(-24.000000, -20.000000)">
<g id="Group-14" transform="translate(16.000000, 20.000000)">
<g id="Group-3" transform="translate(8.000000, 0.000000)">
<g id="Group-31" fill="#000000">
<path d="M11.9903293,24.9097562 L22.8099048,11.1031534 C22.9995344,10.8613757 23.2778836,10.7054392 23.5831534,10.6702222 C23.8894392,10.634836 24.1952169,10.7232169 24.434963,10.9155556 L29.8558207,15.2647539 C30.2865972,15.6103693 30.9159872,15.5413327 31.2616027,15.1105562 C31.4602174,14.8630021 31.529525,14.5358631 31.4483615,14.2290353 L29.9266279,8.47632368 C29.7929674,7.97103771 29.2927373,7.65391298 28.7787201,7.74859865 L10.4890817,11.1176816 C9.99838761,11.208071 9.64940365,11.6463364 9.67119515,12.1448101 L10.2041773,24.3366134 C10.2282982,24.8883711 10.6951401,25.3161053 11.2468978,25.2919844 C11.5386295,25.279231 11.8102122,25.1395993 11.9903293,24.9097562 Z" id="Fill-1"></path>
<path d="M20.9180516,28.9748596 L17.8602219,26.43217 C17.0109171,25.725944 15.7499103,25.8419324 15.0436843,26.6912372 C15.0249589,26.7137564 15.0067304,26.7366841 14.9890119,26.7600039 L12.4245942,30.1351087 C12.0904706,30.5748584 12.1760976,31.2022068 12.6158473,31.5363304 C12.8282888,31.6977442 13.0964249,31.7675698 13.3606122,31.7302754 L20.4184645,30.7339434 C20.9653272,30.6567447 21.3460652,30.150843 21.2688665,29.6039803 C21.2341389,29.3579755 21.1090804,29.1337066 20.9180516,28.9748596 Z" id="Fill-2"></path>
<path d="M19.1700062,23.1674552 L24.1051205,26.9679857 C24.5286802,27.2941689 25.133879,27.2295254 25.4789872,26.8212381 L30.5440949,20.828859 C30.9006188,20.4070661 30.8477078,19.7761158 30.425915,19.419592 C30.4143273,19.4097974 30.4025182,19.4002678 30.3904969,19.3910106 L25.40438,15.5513812 C24.9783089,15.2232791 24.3690491,15.290886 24.0252734,15.7044145 L19.0111683,21.7358925 C18.6581084,22.1605891 18.7161813,22.7910853 19.1408779,23.1441452 C19.1504415,23.1520956 19.1601526,23.159867 19.1700062,23.1674552 Z" id="Fill-3"></path>
<path d="M9.30353231,2.27645283 L4.42237418,4.627066 C3.32561196,5.15523241 2.52904837,6.1540912 2.25817635,7.34088329 L1.05262733,12.6228465 C0.929734796,13.1612849 1.26660125,13.6973995 1.80503961,13.820292 C2.17953648,13.9057668 2.56989397,13.7691724 2.8093916,13.4688481 L6.69883598,8.59157672 L10.5192448,3.80091166 C10.8635876,3.36911722 10.7926941,2.73993362 10.3608996,2.39559077 C10.0605787,2.15609403 9.64961698,2.10978928 9.30353231,2.27645283 Z" id="Fill-4"></path>
<path d="M7.28740675,27.1658943 L6.8479248,16.1392978 C6.8259301,15.5874512 6.36073981,15.1579213 5.8088932,15.179916 C5.51874244,15.1914804 5.24794727,15.3285961 5.06689467,15.5556228 L1.3870983,20.1698196 C0.805299662,20.8993527 0.805293559,21.9342606 1.38708359,22.6638006 L5.50636927,27.8292099 C5.85071365,28.2610032 6.4798975,28.3318944 6.91169073,27.9875501 C7.1605684,27.7890763 7.30008408,27.4839686 7.28740675,27.1658943 Z" id="Fill-5"></path>
<path d="M16.2154826,1.32282996 L12.5912882,5.86747016 C12.2469463,6.2992654 12.3178413,6.92844883 12.7496365,7.27279069 C12.9760118,7.45331722 13.2695209,7.52686276 13.5542742,7.47441145 L24.2981223,5.49540422 C24.8412696,5.39535697 25.2004731,4.87394477 25.1004259,4.33079746 C25.0426062,4.01689958 24.8384171,3.74945966 24.5508458,3.610976 L18.6469016,0.767856098 C17.8062006,0.363005771 16.7972596,0.593297596 16.2154826,1.32282996 Z" id="Fill-6"></path>
</g>
<g id="Group-20" transform="translate(46.000000, 8.000000)">
<g id="Group-12">
<g id="Clip-8"></g>
<polyline id="Fill-7" fill="#000000" points="11.4001509 1.22837736 8.17962264 1.22837736 2.69886792 7.49464151 2.69886792 1.22837736 0 1.22837736 0 15.7077736 2.69886792 15.7077736 2.69886792 11.1375094 3.95320755 9.70898113 8.54701887 15.7077736 11.802566 15.7077736 5.70475472 7.68060377 11.4001509 1.22837736"></polyline>
<path d="M18.4618868,12.3356981 C18.4618868,12.7175849 18.3133585,12.9820377 17.9806792,13.1927547 C17.5855094,13.4430189 17.125434,13.5643774 16.5741887,13.5643774 C16.0283774,13.5643774 15.5695094,13.442717 15.170717,13.1918491 C14.8359245,12.9811321 14.6864906,12.7169811 14.6864906,12.3356981 L14.6864906,5.09222642 L12.0818113,5.09222642 L12.0818113,12.2756226 C12.0818113,13.456 12.5663396,14.3915472 13.5224151,15.056 C14.4238491,15.6821132 15.4490566,16 16.5693585,16 C17.690566,16 18.7175849,15.6787925 19.6223396,15.0457358 C20.5805283,14.3749434 21.066566,13.442717 21.066566,12.2756226 L21.066566,5.09222642 L18.4618868,5.09222642 L18.4618868,12.3356981" id="Fill-9" fill="#000000" mask="url(#mask-2)"></path>
<path d="M26.3963774,13.2724528 L25.5052075,13.2724528 L25.5052075,7.55532075 C26.1575849,7.34279245 26.933434,7.23562264 27.8167547,7.23562264 C28.4718491,7.23562264 28.9189434,7.47018868 29.2241509,7.97433962 C29.568,8.54218868 29.7424906,9.22173585 29.7424906,9.99335849 C29.7424906,11.1746415 29.4895094,12.0256604 28.9904906,12.522566 C28.4911698,13.0203774 27.6184151,13.2724528 26.3963774,13.2724528 Z M31.3772075,6.57750943 C31.0315472,6.03713208 30.5461132,5.60060377 29.933283,5.27939623 C29.3264906,4.96120755 28.6209811,4.8 27.8333585,4.8 C27.0943396,4.80543396 26.3136604,4.92196226 25.5052075,5.14807547 L25.5052075,0.24845283 L22.9005283,0.24845283 L22.9005283,15.7077736 L26.3710189,15.7077736 C28.3915472,15.7077736 29.9091321,15.2181132 30.8809057,14.2526792 C31.8538868,13.2860377 32.3471698,11.8723019 32.3471698,10.0504151 C32.3471698,9.43335849 32.2671698,8.82324528 32.109283,8.23818868 C31.9495849,7.64679245 31.7035472,7.088 31.3772075,6.57750943 Z" id="Fill-10" fill="#000000" mask="url(#mask-2)"></path>
<path d="M38.4144906,7.23562264 C39.0007547,7.23562264 39.4140377,7.43667925 39.714717,7.86837736 C39.9749434,8.24241509 40.1603019,8.70822642 40.2689811,9.25796226 L36.1871698,9.25796226 C36.3178868,8.71215094 36.5500377,8.26626415 36.890566,7.90460377 C37.3144151,7.45418868 37.8128302,7.23562264 38.4144906,7.23562264 Z M42.930717,10.9747925 C42.930717,8.15788679 42.1470189,4.8 38.4144906,4.8 C36.9261887,4.8 35.7080755,5.32075472 34.7933585,6.3474717 C33.8931321,7.35788679 33.4366792,8.71486792 33.4366792,10.3806792 C33.4366792,11.1353962 33.5435472,11.8463396 33.754566,12.4929811 C33.9689057,13.1495849 34.2997736,13.7497358 34.7375094,14.2762264 C35.1870189,14.8169057 35.7862642,15.2455849 36.5189434,15.5495849 C37.2380377,15.8484528 38.0818113,16 39.0300377,16 C40.101434,15.9927547 41.1604528,15.792 42.1775094,15.4022642 L42.4875472,15.2839245 L42.4875472,12.6807547 L41.8031698,12.9965283 C40.9853585,13.3735849 39.9815849,13.5643774 38.8196226,13.5643774 C37.893434,13.5643774 37.234717,13.314717 36.8060377,12.8006038 C36.4630943,12.3897358 36.2366792,11.9468679 36.1207547,11.4575094 L42.930717,11.4575094 L42.930717,10.9747925 Z" id="Fill-11" fill="#000000" mask="url(#mask-2)"></path>
</g>
<path d="M46.3308679,1.1154717 C45.7729811,1.66701887 45.4901132,2.39667925 45.4901132,3.2845283 L45.4901132,5.09222642 L43.9918491,5.09222642 L43.9918491,7.52754717 L45.4901132,7.52754717 L45.4901132,15.7077736 L48.0947925,15.7077736 L48.0947925,7.52754717 L50.6487547,7.52754717 L50.6487547,5.09222642 L48.0947925,5.09222642 L48.0947925,4.14249057 C48.0947925,3.30716981 48.2460377,3.02550943 48.3112453,2.94339623 C48.3526038,2.89116981 48.5421887,2.72120755 49.2519245,2.72120755 L51.242566,2.72120755 L51.242566,0.286188679 L48.3852075,0.286188679 C47.578566,0.286188679 46.8869434,0.565433962 46.3308679,1.1154717" id="Fill-13" fill="#000000"></path>
<g id="Group-17" transform="translate(52.226415, 0.000000)">
<g id="Clip-15"></g>
<polygon id="Fill-14" fill="#000000" points="0.19954717 15.7077736 2.8045283 15.7077736 2.8045283 0.267471698 0.19954717 0.267471698"></polygon>
<path d="M10.1862642,13.5643774 C9.25222642,13.5643774 8.52981132,13.2721509 7.9770566,12.6698868 C7.41373585,12.0561509 7.13962264,11.3138113 7.13962264,10.4 C7.13962264,9.48649057 7.41373585,8.74384906 7.97735849,8.12981132 C8.52981132,7.52784906 9.25222642,7.23562264 10.1862642,7.23562264 C11.1088302,7.23562264 11.8288302,7.52845283 12.3876226,8.13162264 C12.9566792,8.74535849 13.2332075,9.48739623 13.2332075,10.4 C13.2332075,11.3126038 12.9566792,12.0543396 12.3876226,12.6683774 C11.8288302,13.2715472 11.1088302,13.5643774 10.1862642,13.5643774 Z M10.1862642,4.8 C8.54701887,4.8 7.1794717,5.34671698 6.12256604,6.42445283 C5.06898113,7.49977358 4.53464151,8.83713208 4.53464151,10.4 C4.53464151,11.9628679 5.06716981,13.2999245 6.11743396,14.3749434 C7.17101887,15.4529811 8.53977358,16 10.1862642,16 C11.8113208,16 13.1749434,15.4499623 14.2384906,14.3661887 C15.2999245,13.2848302 15.8381887,11.9501887 15.8381887,10.4 C15.8381887,8.85011321 15.2999245,7.51577358 14.2384906,6.43350943 C13.1746415,5.34973585 11.8110189,4.8 10.1862642,4.8 Z" id="Fill-16" fill="#000000" mask="url(#mask-4)"></path>
</g>
<polyline id="Fill-18" fill="#000000" points="80.8470943 5.09222642 78.9838491 11.4943396 77.1287547 5.09222642 74.7882264 5.09222642 72.9101887 11.4910189 71.0638491 5.09222642 68.3100377 5.09222642 71.6392453 15.7077736 74.3142642 15.7077736 76.0009057 9.61237736 77.6944906 15.7077736 80.2629434 15.7077736 83.6006038 5.09222642 80.8470943 5.09222642"></polyline>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,22 +1,14 @@
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
}
]
};
}
import { BrowserRouter as Router } from 'react-router-dom';
import App from './app/App';
const root = ReactDOM.createRoot(document.getElementById('root') as Element);
root.render(
<React.StrictMode>
<App />
<Router>
<App />
</Router>
</React.StrictMode>
);

View File

@ -1,14 +0,0 @@
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')
]
};

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"baseUrl": ".",
"baseUrl": "./src",
"rootDir": ".",
"outDir": "dist",
"module": "esnext",
@ -12,21 +12,17 @@
"sourceMap": true,
"jsx": "react",
"moduleResolution": "node",
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": false,
"noImplicitAny": true,
"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
@ -36,7 +32,6 @@
"**/*.tsx",
"**/*.jsx",
"**/*.js",
".eslintrc.js"
],
"exclude": [
"node_modules"