diff --git a/clients/ui/frontend/Dockerfile b/clients/ui/frontend/Dockerfile
index 2b20d06f..0e787fde 100644
--- a/clients/ui/frontend/Dockerfile
+++ b/clients/ui/frontend/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:18 AS build-stage
+FROM node:20 AS build-stage
WORKDIR /usr/src/app
diff --git a/clients/ui/frontend/config/webpack.common.js b/clients/ui/frontend/config/webpack.common.js
index ddbf702b..6da6aa23 100644
--- a/clients/ui/frontend/config/webpack.common.js
+++ b/clients/ui/frontend/config/webpack.common.js
@@ -5,8 +5,8 @@ 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 {
@@ -35,42 +35,56 @@ module.exports = (env) => {
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,
@@ -103,6 +117,17 @@ 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',
+ ],
}
]
},
@@ -120,7 +145,7 @@ module.exports = (env) => {
silent: true
}),
new CopyPlugin({
- patterns: [{ from: './src/favicon.png', to: 'images' }]
+ patterns: [{ from: './src/images', to: 'images' }]
})
],
resolve: {
diff --git a/clients/ui/frontend/config/webpack.dev.js b/clients/ui/frontend/config/webpack.dev.js
index 311a9119..f7a6c806 100644
--- a/clients/ui/frontend/config/webpack.dev.js
+++ b/clients/ui/frontend/config/webpack.dev.js
@@ -20,28 +20,30 @@ module.exports = merge(common('development'), {
historyApiFallback: true,
open: true,
static: {
- directory: path.resolve(relativeDir, 'dist')
+ directory: path.resolve(relativeDir, 'dist'),
},
client: {
- overlay: true
+ overlay: true,
},
- proxy: {
- '/api': {
+ 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'],
+ },
+ ],
+ },
});
diff --git a/clients/ui/frontend/docs/dev-setup.md b/clients/ui/frontend/docs/dev-setup.md
index 4ea61f8b..327a4057 100644
--- a/clients/ui/frontend/docs/dev-setup.md
+++ b/clients/ui/frontend/docs/dev-setup.md
@@ -5,8 +5,8 @@
This project requires the following tools to be installed on your system:
- [NodeJS and NPM](https://nodejs.org/)
- - Node recommended version -> `18.16.0`
- - NPM recommended version -> `9.6.7`
+ - Node recommended version -> `20.17.0`
+ - NPM recommended version -> `10.8.2`
### Additional tooling
diff --git a/clients/ui/frontend/package-lock.json b/clients/ui/frontend/package-lock.json
index 977275c7..f7d4aca9 100644
--- a/clients/ui/frontend/package-lock.json
+++ b/clients/ui/frontend/package-lock.json
@@ -15,7 +15,8 @@
"lodash-es": "^4.17.21",
"npm-run-all": "^4.1.5",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "sass": "^1.78.0"
},
"devDependencies": {
"@babel/preset-env": "^7.21.5",
@@ -56,6 +57,7 @@
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.14.1",
"rimraf": "^6.0.1",
+ "sass-loader": "^16.0.1",
"serve": "^14.2.1",
"style-loader": "^4.0.0",
"svg-url-loader": "^8.0.0",
@@ -73,7 +75,7 @@
"webpack-merge": "^6.0.1"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"optionalDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.1",
@@ -4874,7 +4876,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
- "dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -5504,7 +5505,6 @@
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
- "dev": true,
"engines": {
"node": ">=8"
}
@@ -6115,7 +6115,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
- "dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -6139,7 +6138,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -6151,7 +6149,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -6163,7 +6160,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -6175,7 +6171,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
"engines": {
"node": ">=0.12.0"
}
@@ -6184,7 +6179,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
"dependencies": {
"is-number": "^7.0.0"
},
@@ -6795,6 +6789,7 @@
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz",
"integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"icss-utils": "^5.1.0",
"postcss": "^8.4.33",
@@ -10200,7 +10195,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -11085,6 +11079,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/immutable": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
+ "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
+ "license": "MIT"
+ },
"node_modules/import-fresh": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
@@ -11272,7 +11272,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -11397,7 +11396,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11451,7 +11449,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "devOptional": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -15515,7 +15512,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -16389,7 +16385,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
- "devOptional": true,
"engines": {
"node": ">=8.6"
},
@@ -17463,7 +17458,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
- "dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -17965,6 +17959,64 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "node_modules/sass": {
+ "version": "1.78.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz",
+ "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/sass-loader": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz",
+ "integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "neo-async": "^2.6.2"
+ },
+ "engines": {
+ "node": ">= 18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ },
+ "peerDependencies": {
+ "@rspack/core": "0.x || 1.x",
+ "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0",
+ "sass": "^1.3.0",
+ "sass-embedded": "*",
+ "webpack": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "node-sass": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -18542,7 +18594,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18997,6 +19048,7 @@
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz",
"integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 18.12.0"
},
diff --git a/clients/ui/frontend/package.json b/clients/ui/frontend/package.json
index 67e8bbbb..408bb28c 100644
--- a/clients/ui/frontend/package.json
+++ b/clients/ui/frontend/package.json
@@ -7,7 +7,7 @@
"license": "Apache-2.0",
"private": true,
"engines": {
- "node": ">=18.0.0"
+ "node": ">=20.0.0"
},
"scripts": {
"build": "run-s build:prod",
@@ -69,6 +69,7 @@
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.14.1",
"rimraf": "^6.0.1",
+ "sass-loader": "^16.0.1",
"serve": "^14.2.1",
"style-loader": "^4.0.0",
"svg-url-loader": "^8.0.0",
@@ -92,7 +93,8 @@
"lodash-es": "^4.17.21",
"npm-run-all": "^4.1.5",
"react": "^18",
- "react-dom": "^18"
+ "react-dom": "^18",
+ "sass": "^1.78.0"
},
"optionalDependencies": {
"@typescript-eslint/eslint-plugin": "^7.1.1",
diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/home.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/home.ts
index 4ac1ddd0..4299e844 100644
--- a/clients/ui/frontend/src/__tests__/cypress/cypress/pages/home.ts
+++ b/clients/ui/frontend/src/__tests__/cypress/cypress/pages/home.ts
@@ -3,8 +3,8 @@ class Home {
cy.visit(`/`);
}
- findButton() {
- return cy.get('button:contains("Primary Action")');
+ findTitle() {
+ cy.get(`h1`).should(`have.text`, `Model registry`);
}
}
diff --git a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts
index 40d531b3..ae7d99d3 100644
--- a/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts
+++ b/clients/ui/frontend/src/__tests__/cypress/cypress/tests/mocked/application.cy.ts
@@ -8,6 +8,6 @@ describe('Application', () => {
it('Home page should have primary button', () => {
home.visit();
- home.findButton();
+ home.findTitle();
});
});
diff --git a/clients/ui/frontend/src/app/App.tsx b/clients/ui/frontend/src/app/App.tsx
index b4db3ef8..2a8a7a81 100644
--- a/clients/ui/frontend/src/app/App.tsx
+++ b/clients/ui/frontend/src/app/App.tsx
@@ -3,11 +3,13 @@ import '@patternfly/react-core/dist/styles/base.css';
import './app.css';
import {
Alert,
+ Brand,
Bullseye,
Button,
- Flex,
Masthead,
+ MastheadBrand,
MastheadContent,
+ MastheadMain,
MastheadToggle,
Page,
PageSection,
@@ -15,7 +17,6 @@ import {
Spinner,
Stack,
StackItem,
- Title,
} from '@patternfly/react-core';
import { BarsIcon } from '@patternfly/react-icons';
import NavSidebar from './NavSidebar';
@@ -76,19 +77,22 @@ const App: React.FC = () => {
const masthead = (
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
- Kubeflow Model Registry UI
-
-
-
+ {/* TODO: Change this into a component for Header Tools */}
);
diff --git a/clients/ui/frontend/src/app/AppRoutes.tsx b/clients/ui/frontend/src/app/AppRoutes.tsx
index 015a897f..10051e6d 100644
--- a/clients/ui/frontend/src/app/AppRoutes.tsx
+++ b/clients/ui/frontend/src/app/AppRoutes.tsx
@@ -1,9 +1,8 @@
import * as React from 'react';
import { Route, Routes } from 'react-router-dom';
-import { Dashboard } from './Dashboard/Dashboard';
-import { Support } from './Support/Support';
-import { NotFound } from './NotFound/NotFound';
-import { Admin } from './Settings/Admin';
+import { NotFound } from './pages/notFound/NotFound';
+import ModelRegistrySettingsRoutes from './pages/settings/ModelRegistrySettingsRoutes';
+import ModelRegistryRoutes from './pages/modelRegistry/ModelRegistryRoutes';
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
'children' in navItem;
@@ -35,24 +34,16 @@ export const useAdminSettings = (): NavDataItem[] => {
return [
{
label: 'Settings',
- children: [
- { label: 'Setting 1', path: '/admin' },
- { label: 'Setting 2', path: '/admin' },
- { label: 'Setting 3', path: '/admin' },
- ],
+ children: [{ label: 'Model Registry', path: '/settings' }],
},
];
};
export const useNavData = (): NavDataItem[] => [
{
- label: 'Dashboard',
+ label: 'Model Registry',
path: '/',
},
- {
- label: 'Support',
- path: '/support',
- },
...useAdminSettings(),
];
@@ -61,13 +52,12 @@ const AppRoutes: React.FC = () => {
return (
- } />
- } />
+ } />
} />
{
// TODO: Remove the linter skip when we implement authentication
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- isAdmin && } />
+ isAdmin && } />
}
);
diff --git a/clients/ui/frontend/src/app/Dashboard/Dashboard.tsx b/clients/ui/frontend/src/app/Dashboard/Dashboard.tsx
deleted file mode 100644
index 3fc01718..00000000
--- a/clients/ui/frontend/src/app/Dashboard/Dashboard.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as React from 'react';
-import {
- Button,
- Content,
- ContentVariants,
- EmptyState,
- EmptyStateBody,
- EmptyStateFooter,
- EmptyStateVariant,
- PageSection,
-} from '@patternfly/react-core';
-import { CubesIcon } from '@patternfly/react-icons';
-
-const Dashboard: React.FunctionComponent = () => (
-
-
-
-
- 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.
-
-
-
-
-
-
-
-);
-
-export { Dashboard };
diff --git a/clients/ui/frontend/src/app/Settings/Admin.tsx b/clients/ui/frontend/src/app/Settings/Admin.tsx
deleted file mode 100644
index 6b1931f1..00000000
--- a/clients/ui/frontend/src/app/Settings/Admin.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as React from 'react';
-import { CubesIcon } from '@patternfly/react-icons';
-import {
- Button,
- Content,
- ContentVariants,
- EmptyState,
- EmptyStateBody,
- EmptyStateFooter,
- EmptyStateVariant,
- PageSection,
-} from '@patternfly/react-core';
-
-const Admin: React.FunctionComponent = () => (
-
-
-
-
- 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.
-
-
-
-
-
-
-
-);
-
-export { Admin };
diff --git a/clients/ui/frontend/src/app/Support/Support.tsx b/clients/ui/frontend/src/app/Support/Support.tsx
deleted file mode 100644
index 000f2745..00000000
--- a/clients/ui/frontend/src/app/Support/Support.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import * as React from 'react';
-import { CubesIcon } from '@patternfly/react-icons';
-import {
- Button,
- Content,
- ContentVariants,
- EmptyState,
- EmptyStateBody,
- EmptyStateFooter,
- EmptyStateVariant,
- PageSection,
-} from '@patternfly/react-core';
-
-const Support: React.FunctionComponent = () => (
-
-
-
-
- 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.
-
-
-
-
-
-
-
-);
-
-export { Support };
diff --git a/clients/ui/frontend/src/app/bgimages/.gitkeep b/clients/ui/frontend/src/app/bgimages/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/clients/ui/frontend/src/app/components/ApplicationsPage.tsx b/clients/ui/frontend/src/app/components/ApplicationsPage.tsx
new file mode 100644
index 00000000..346e207f
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/ApplicationsPage.tsx
@@ -0,0 +1,154 @@
+import React from 'react';
+import { ExclamationCircleIcon, QuestionCircleIcon } from '@patternfly/react-icons';
+import {
+ PageSection,
+ Content,
+ EmptyState,
+ EmptyStateVariant,
+ Spinner,
+ EmptyStateBody,
+ PageBreadcrumb,
+ StackItem,
+ Stack,
+ Flex,
+} from '@patternfly/react-core';
+
+type ApplicationsPageProps = {
+ title?: React.ReactNode;
+ breadcrumb?: React.ReactNode;
+ description?: React.ReactNode;
+ loaded: boolean;
+ empty: boolean;
+ loadError?: Error;
+ children?: React.ReactNode;
+ errorMessage?: string;
+ emptyMessage?: string;
+ emptyStatePage?: React.ReactNode;
+ headerAction?: React.ReactNode;
+ headerContent?: React.ReactNode;
+ provideChildrenPadding?: boolean;
+ removeChildrenTopPadding?: boolean;
+ subtext?: React.ReactNode;
+ loadingContent?: React.ReactNode;
+ noHeader?: boolean;
+};
+
+const ApplicationsPage: React.FC = ({
+ title,
+ breadcrumb,
+ description,
+ loaded,
+ empty,
+ loadError,
+ children,
+ errorMessage,
+ emptyMessage,
+ emptyStatePage,
+ headerAction,
+ headerContent,
+ provideChildrenPadding,
+ removeChildrenTopPadding,
+ subtext,
+ loadingContent,
+ noHeader,
+}) => {
+ const renderHeader = () => (
+
+
+
+
+
+
+ {title}
+
+
+ {subtext && {subtext}}
+ {description && {description}}
+
+
+ {headerAction}
+
+
+ {headerContent && {headerContent}}
+
+
+ );
+
+ const renderContents = () => {
+ if (loadError) {
+ return (
+
+
+ {loadError.message}
+
+
+ );
+ }
+
+ if (!loaded) {
+ return (
+ loadingContent || (
+
+
+
+
+
+ )
+ );
+ }
+
+ if (empty) {
+ return !emptyStatePage ? (
+
+
+
+ ) : (
+ emptyStatePage
+ );
+ }
+
+ if (provideChildrenPadding) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return children;
+ };
+
+ return (
+ <>
+ {breadcrumb && {breadcrumb}}
+ {!noHeader && renderHeader()}
+ {renderContents()}
+ >
+ );
+};
+
+export default ApplicationsPage;
diff --git a/clients/ui/frontend/src/app/components/design/CollapsibleSection.tsx b/clients/ui/frontend/src/app/components/design/CollapsibleSection.tsx
new file mode 100644
index 00000000..0228052f
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/CollapsibleSection.tsx
@@ -0,0 +1,71 @@
+import * as React from 'react';
+import { Button, Flex, FlexItem, Content, ContentVariants } from '@patternfly/react-core';
+import { AngleDownIcon, AngleRightIcon } from '@patternfly/react-icons';
+
+interface CollapsibleSectionProps {
+ open?: boolean;
+ setOpen?: (update: boolean) => void;
+ title: string;
+ titleVariant?: ContentVariants.h1 | ContentVariants.h2;
+ children?: React.ReactNode;
+ id?: string;
+ showChildrenWhenClosed?: boolean;
+}
+
+const CollapsibleSection: React.FC = ({
+ open,
+ setOpen,
+ title,
+ titleVariant = ContentVariants.h2,
+ children,
+ id,
+ showChildrenWhenClosed,
+}) => {
+ const [innerOpen, setInnerOpen] = React.useState(true);
+ const localId = id || title.replace(/ /g, '-');
+ const titleId = `${localId}-title`;
+
+ return (
+ <>
+
+
+ : }
+ aria-labelledby={titleId}
+ aria-expanded={open}
+ variant="plain"
+ style={{
+ paddingLeft: 0,
+ paddingRight: 0,
+ fontSize:
+ titleVariant === ContentVariants.h2
+ ? 'var(--pf-v6-global--FontSize--xl)'
+ : 'var(--pf-v6-global--FontSize--2xl)',
+ }}
+ onClick={() => (setOpen ? setOpen(!open) : setInnerOpen((prev) => !prev))}
+ />
+
+
+
+
+ {title}
+
+
+
+
+ {(open ?? innerOpen) || showChildrenWhenClosed ? children : null}
+ >
+ );
+};
+
+export default CollapsibleSection;
diff --git a/clients/ui/frontend/src/app/components/design/DividedGallery.scss b/clients/ui/frontend/src/app/components/design/DividedGallery.scss
new file mode 100644
index 00000000..e0a443f9
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/DividedGallery.scss
@@ -0,0 +1,27 @@
+.odh-divided-gallery {
+ position: relative;
+ background-color: var(--pf-v5-global--BackgroundColor--100);
+
+ &__border {
+ width: 2px;
+ position: absolute;
+ top: var(--pf-v5-global--spacer--md);
+ bottom: var(--pf-v5-global--spacer--md);
+ left: 0;
+ background-color: white;
+ content: ' ';
+ }
+ &__item {
+ border-left: 1px solid var(--pf-v5-global--BorderColor--100);
+ padding: 0 var(--pf-v5-global--spacer--xl);
+ margin: var(--pf-v5-global--spacer--md) 0;
+ }
+ .pf-v5-l-gallery {
+ position: relative;
+ }
+ &__close {
+ position: absolute;
+ top: var(--pf-v5-global--spacer--xs);
+ right: 0;
+ }
+}
diff --git a/clients/ui/frontend/src/app/components/design/DividedGallery.tsx b/clients/ui/frontend/src/app/components/design/DividedGallery.tsx
new file mode 100644
index 00000000..4f084790
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/DividedGallery.tsx
@@ -0,0 +1,52 @@
+import * as React from 'react';
+import { Button, Gallery, GalleryProps } from '@patternfly/react-core';
+import { css } from '@patternfly/react-styles';
+import { TimesIcon } from '@patternfly/react-icons';
+
+type DividedGalleryProps = Omit & {
+ minSize: string;
+ itemCount: number;
+ showClose?: boolean;
+ closeAlt?: string;
+ onClose?: () => void;
+ closeTestId?: string;
+};
+
+import './DividedGallery.scss';
+
+const DividedGallery: React.FC = ({
+ minSize,
+ itemCount,
+ showClose,
+ closeAlt,
+ onClose,
+ children,
+ className,
+ closeTestId,
+ ...rest
+}) => (
+
+
+
+ {children}
+ {showClose ? (
+
+
+
+ ) : null}
+
+
+);
+
+export default DividedGallery;
diff --git a/clients/ui/frontend/src/app/components/design/DividedGalleryItem.tsx b/clients/ui/frontend/src/app/components/design/DividedGalleryItem.tsx
new file mode 100644
index 00000000..af82c87e
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/DividedGalleryItem.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import { GalleryItemProps } from '@patternfly/react-core';
+import { css } from '@patternfly/react-styles';
+
+import './DividedGallery.scss';
+
+const DividedGalleryItem: React.FC = ({ className, ...rest }) => (
+
+);
+
+export default DividedGalleryItem;
diff --git a/clients/ui/frontend/src/app/components/design/HeaderIcon.tsx b/clients/ui/frontend/src/app/components/design/HeaderIcon.tsx
new file mode 100644
index 00000000..e0e8ebf0
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/HeaderIcon.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import {
+ sectionTypeBackgroundColor,
+ typedBackgroundColor,
+ typedObjectImage,
+ ProjectObjectType,
+ SectionType,
+} from '~/app/components/design/utils';
+
+interface HeaderIconProps {
+ size?: number;
+ padding?: number;
+ image?: string;
+ type: ProjectObjectType;
+ sectionType?: SectionType;
+}
+
+const HeaderIcon: React.FC = ({
+ size = 40,
+ padding = 2,
+ image,
+ type,
+ sectionType,
+}) => (
+
+
+
+);
+
+export default HeaderIcon;
diff --git a/clients/ui/frontend/src/app/components/design/InfoGalleryItem.tsx b/clients/ui/frontend/src/app/components/design/InfoGalleryItem.tsx
new file mode 100644
index 00000000..3ce5f3f1
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/InfoGalleryItem.tsx
@@ -0,0 +1,85 @@
+import * as React from 'react';
+import {
+ Button,
+ ButtonVariant,
+ Flex,
+ FlexItem,
+ GalleryItemProps,
+ Stack,
+ StackItem,
+} from '@patternfly/react-core';
+import { SectionType, sectionTypeBackgroundColor } from '~/app/components/design/utils';
+import DividedGalleryItem from '~/app/components/design/DividedGalleryItem';
+
+const HEADER_ICON_SIZE = 40;
+const HEADER_ICON_PADDING = 2;
+
+type InfoGalleryItemProps = {
+ title: string;
+ imgSrc: string;
+ sectionType: SectionType;
+ description: React.ReactNode;
+ isOpen: boolean;
+ onClick?: () => void;
+ testId?: string;
+} & GalleryItemProps;
+
+const InfoGalleryItem: React.FC = ({
+ title,
+ imgSrc,
+ sectionType,
+ description,
+ isOpen,
+ onClick,
+ testId,
+ ...rest
+}) => (
+
+
+
+
+
+
+
+ {onClick ? (
+
+ ) : (
+ {title}
+ )}
+
+
+ {isOpen ? {description} : null}
+
+
+);
+
+export default InfoGalleryItem;
diff --git a/clients/ui/frontend/src/app/components/design/ScrolledGallery.tsx b/clients/ui/frontend/src/app/components/design/ScrolledGallery.tsx
new file mode 100644
index 00000000..3d619970
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/ScrolledGallery.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+
+type ScrolledGalleryProps = {
+ count: number;
+ childWidth: string;
+} & React.HTMLProps;
+
+const ScrolledGallery: React.FC = ({
+ children,
+ count,
+ childWidth,
+ ...rest
+}) => {
+ let gridTemplateColumns = childWidth;
+ for (let i = 1; i < count; i++) {
+ gridTemplateColumns = `${gridTemplateColumns} ${childWidth}`;
+ }
+ return (
+
+ {children}
+
+ );
+};
+
+export default ScrolledGallery;
diff --git a/clients/ui/frontend/src/app/components/design/TitleWithIcon.tsx b/clients/ui/frontend/src/app/components/design/TitleWithIcon.tsx
new file mode 100644
index 00000000..02893ae0
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/TitleWithIcon.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import { Flex, FlexItem } from '@patternfly/react-core';
+import {
+ ProjectObjectType,
+ typedBackgroundColor,
+ typedObjectImage,
+} from '~/app/components/design/utils';
+
+interface TitleWithIconProps {
+ title: React.ReactNode;
+ objectType: ProjectObjectType;
+ iconSize?: number;
+ padding?: number;
+}
+
+const TitleWithIcon: React.FC = ({
+ title,
+ objectType,
+ iconSize = 40,
+ padding = 4,
+}) => (
+
+
+
+
+
+
+ {title}
+
+);
+
+export default TitleWithIcon;
diff --git a/clients/ui/frontend/src/app/components/design/TypeBorderCard.scss b/clients/ui/frontend/src/app/components/design/TypeBorderCard.scss
new file mode 100644
index 00000000..8edc0f3b
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/TypeBorderCard.scss
@@ -0,0 +1,141 @@
+.odh-type-bordered-card {
+ position: relative;
+ border-radius: 16px;
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
+ padding: 1px;
+
+ &:after {
+ position: absolute;
+ width: 48px;
+ height: 4px;
+ background: transparent;
+ top: 0;
+ left: calc(50% - 24px);
+ border-radius: 0 0 4px 4px;
+ content: ' ';
+ }
+ &.project {
+ &:after {
+ background: var(--ai-project--BorderColor);
+ }
+ }
+ &.notebook {
+ &:after {
+ background: var(--ai-notebook--BorderColor);
+ }
+ }
+ &.pipeline {
+ &:after {
+ background: var(--ai-pipeline--BorderColor);
+ }
+ }
+ &.pipeline-run {
+ &:after {
+ background: var(--ai-pipeline--BorderColor);
+ }
+ }
+ &.cluster-storage {
+ &:after {
+ background: var(--ai-cluster-storage--BorderColor);
+ }
+ }
+ &.model-server {
+ &:after {
+ background: var(--ai-model-server--BorderColor);
+ }
+ }
+ &.data-connection {
+ &:after {
+ background: var(--ai-data-connection--BorderColor);
+ }
+ }
+ &.user {
+ &:after {
+ background: var(--ai-user--BorderColor);
+ }
+ }
+ &.group {
+ &:after {
+ background: var(--ai-group--BorderColor);
+ }
+ }
+ &.set-up {
+ &:after {
+ background: var(--ai-set-up--BorderColor);
+ }
+ }
+ &.organize {
+ &:after {
+ background: var(--ai-organize--BorderColor);
+ }
+ }
+ &.training {
+ &:after {
+ background: var(--ai-training--BorderColor);
+ }
+ }
+ &.serving {
+ &:after {
+ background: var(--ai-serving--BorderColor);
+ }
+ }
+ &.m-is-selectable {
+ &:after {
+ display: none;
+ top: unset;
+ bottom: 0;
+ transform: rotate( -180deg );
+ }
+ &:hover {
+ cursor: pointer;
+ }
+ &:hover, &.m-is-selected {
+ &:after {
+ display: block
+ }
+ &.project {
+ border-color: var(--ai-project--BorderColor);
+ }
+ &.notebook {
+ border-color: var(--ai-notebook--BorderColor);
+ }
+ &.pipeline {
+ border-color: var(--ai-pipeline--BorderColor);
+ }
+ &.pipeline-run {
+ border-color: var(--ai-pipeline--BorderColor);
+ }
+ &.cluster-storage {
+ border-color: var(--ai-cluster-storage--BorderColor);
+ }
+ &.model-server {
+ border-color: var(--ai-model-server--BorderColor);
+ }
+ &.data-connection {
+ border-color: var(--ai-data-connection--BorderColor);
+ }
+ &.user {
+ border-color: var(--ai-user--BorderColor);
+ }
+ &.group {
+ border-color: var(--ai-group--BorderColor);
+ }
+ &.set-up {
+ border-color: var(--ai-set-up--BorderColor);
+ }
+ &.organize {
+ border-color: var(--ai-organize--BorderColor);
+ }
+ &.training {
+ border-color: var(--ai-training--BorderColor);
+ }
+ &.serving {
+ border-color: var(--ai-serving--BorderColor);
+ }
+ }
+ &.m-is-selected {
+ border-width: 2px;
+ padding: 0;
+ }
+ }
+}
diff --git a/clients/ui/frontend/src/app/components/design/TypeBorderedCard.tsx b/clients/ui/frontend/src/app/components/design/TypeBorderedCard.tsx
new file mode 100644
index 00000000..9567a400
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/TypeBorderedCard.tsx
@@ -0,0 +1,37 @@
+import * as React from 'react';
+import { css } from '@patternfly/react-styles';
+import { Card, CardProps } from '@patternfly/react-core';
+import { ProjectObjectType, SectionType } from '~/app/components/design/utils';
+
+import './TypeBorderCard.scss';
+
+type TypeBorderedCardProps = CardProps & {
+ objectType?: ProjectObjectType;
+ sectionType?: SectionType;
+ selectable?: boolean;
+ selected?: boolean;
+};
+const TypeBorderedCard: React.FC = ({
+ objectType,
+ sectionType,
+ className,
+ selectable,
+ selected,
+ ...rest
+}) => (
+
+);
+
+export default TypeBorderedCard;
diff --git a/clients/ui/frontend/src/app/components/design/utils.ts b/clients/ui/frontend/src/app/components/design/utils.ts
new file mode 100644
index 00000000..ef4f4114
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/utils.ts
@@ -0,0 +1,34 @@
+import registerModelImg from '~/images/UI_icon-Cubes-RGB.svg';
+import modelRegistryEmptyStateImg from '~/images/empty-state-model-registries.svg';
+import './vars.scss';
+
+export enum ProjectObjectType {
+ registeredModels = 'registered-models',
+}
+
+export const typedBackgroundColor = (objectType: ProjectObjectType): string => {
+ switch (objectType) {
+ case ProjectObjectType.registeredModels:
+ return 'var(--ai-model-server--BackgroundColor)';
+ default:
+ return '';
+ }
+};
+
+export const typedObjectImage = (objectType: ProjectObjectType): string => {
+ switch (objectType) {
+ case ProjectObjectType.registeredModels:
+ return registerModelImg;
+ default:
+ return '';
+ }
+};
+
+export const typedEmptyImage = (objectType: ProjectObjectType): string => {
+ switch (objectType) {
+ case ProjectObjectType.registeredModels:
+ return modelRegistryEmptyStateImg;
+ default:
+ return '';
+ }
+};
diff --git a/clients/ui/frontend/src/app/components/design/vars.scss b/clients/ui/frontend/src/app/components/design/vars.scss
new file mode 100644
index 00000000..bfff3860
--- /dev/null
+++ b/clients/ui/frontend/src/app/components/design/vars.scss
@@ -0,0 +1,31 @@
+:root {
+ --ai-set-up--BackgroundColor: #ffe8cc;
+ --ai-organize--BackgroundColor: #ffe8cc;
+ --ai-training--BackgroundColor: #daf2f2;
+ --ai-serving--BackgroundColor: #e0f0ff;
+
+ --ai-set-up--BorderColor: #f8ae54;
+ --ai-organize--BorderColor: #f8ae54;
+ --ai-training--BorderColor: #9ad8d8;
+ --ai-serving--BorderColor: #92c5f9;
+
+ --ai-project--BackgroundColor: var(--ai-organize--BackgroundColor);
+ --ai-project--BorderColor: var(--ai-organize--BorderColor);
+ --ai-data-connection--BackgroundColor: var(--ai-organize--BackgroundColor);
+ --ai-data-connection--BorderColor: var(--ai-organize--BorderColor);
+ --ai-cluster-storage--BackgroundColor: var(--ai-organize--BackgroundColor);
+ --ai-cluster-storage--BorderColor: var(--ai-organize--BorderColor);
+
+ --ai-notebook--BackgroundColor: var(--ai-training--BackgroundColor);
+ --ai-notebook--BorderColor: var(--ai-training--BorderColor);
+ --ai-pipeline--BackgroundColor: var(--ai-training--BackgroundColor);
+ --ai-pipeline--BorderColor: var(--ai-training--BorderColor);
+
+ --ai-model-server--BackgroundColor: var(--ai-serving--BackgroundColor);
+ --ai-model-server--BorderColor: var(--ai-serving--BorderColor);
+
+ --ai-user--BackgroundColor: var(--ai-set-up--BackgroundColor);
+ --ai-user--BorderColor: var(--ai-set-up--BorderColor);
+ --ai-group--BackgroundColor: var(--ai-set-up--BackgroundColor);
+ --ai-group--BorderColor: var(--ai-set-up--BorderColor);
+}
diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistry.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistry.tsx
new file mode 100644
index 00000000..a9edd66c
--- /dev/null
+++ b/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistry.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import ApplicationsPage from '~/app/components/ApplicationsPage';
+import TitleWithIcon from '~/app/components/design/TitleWithIcon';
+import { ProjectObjectType } from '~/app/components/design/utils';
+
+type ModelRegistryProps = Omit<
+ React.ComponentProps,
+ | 'title'
+ | 'description'
+ | 'loadError'
+ | 'loaded'
+ | 'provideChildrenPadding'
+ | 'removeChildrenTopPadding'
+ | 'headerContent'
+>;
+
+const ModelRegistry: React.FC = ({ ...pageProps }) => {
+ const [loaded, loadError] = [true, undefined]; // TODO: change with real usage
+
+ return (
+
+ }
+ description="Select a model registry to view and manage your registered models. Model registries provide a structured and organized way to store, share, version, deploy, and track models."
+ loadError={loadError}
+ loaded={loaded}
+ provideChildrenPadding
+ removeChildrenTopPadding
+ />
+ );
+};
+
+export default ModelRegistry;
diff --git a/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistryRoutes.tsx b/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistryRoutes.tsx
new file mode 100644
index 00000000..1d3e4c0c
--- /dev/null
+++ b/clients/ui/frontend/src/app/pages/modelRegistry/ModelRegistryRoutes.tsx
@@ -0,0 +1,11 @@
+import * as React from 'react';
+import { Route, Routes } from 'react-router-dom';
+import ModelRegistry from './ModelRegistry';
+
+const ModelRegistryRoutes: React.FC = () => (
+
+ } />
+
+);
+
+export default ModelRegistryRoutes;
diff --git a/clients/ui/frontend/src/app/NotFound/NotFound.tsx b/clients/ui/frontend/src/app/pages/notFound/NotFound.tsx
similarity index 100%
rename from clients/ui/frontend/src/app/NotFound/NotFound.tsx
rename to clients/ui/frontend/src/app/pages/notFound/NotFound.tsx
diff --git a/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettings.tsx b/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettings.tsx
new file mode 100644
index 00000000..f48f0bdf
--- /dev/null
+++ b/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettings.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { EmptyState, EmptyStateBody, EmptyStateVariant } from '@patternfly/react-core';
+import { PlusCircleIcon } from '@patternfly/react-icons';
+import ApplicationsPage from '~/app/components/ApplicationsPage';
+
+const ModelRegistrySettings: React.FC = () => {
+ const [modelRegistries, loaded, loadError] = [[], true, undefined]; // TODO: change to real values
+ return (
+ <>
+
+ To get started, create a model registry.
+
+ }
+ provideChildrenPadding
+ >
+ TODO: Add model registry settings
+
+ >
+ );
+};
+
+export default ModelRegistrySettings;
diff --git a/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettingsRoutes.tsx b/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettingsRoutes.tsx
new file mode 100644
index 00000000..8e6b7500
--- /dev/null
+++ b/clients/ui/frontend/src/app/pages/settings/ModelRegistrySettingsRoutes.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import { Navigate, Routes, Route } from 'react-router-dom';
+import ModelRegistrySettings from './ModelRegistrySettings';
+
+const ModelRegistrySettingsRoutes: React.FC = () => (
+
+ } />
+ } />
+
+);
+
+export default ModelRegistrySettingsRoutes;
diff --git a/clients/ui/frontend/src/favicon.png b/clients/ui/frontend/src/favicon.png
deleted file mode 100644
index 28998a8c..00000000
Binary files a/clients/ui/frontend/src/favicon.png and /dev/null differ
diff --git a/clients/ui/frontend/src/images/UI_icon-Cubes-RGB.svg b/clients/ui/frontend/src/images/UI_icon-Cubes-RGB.svg
new file mode 100644
index 00000000..b1c8c577
--- /dev/null
+++ b/clients/ui/frontend/src/images/UI_icon-Cubes-RGB.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/clients/ui/frontend/src/images/empty-state-model-registries.svg b/clients/ui/frontend/src/images/empty-state-model-registries.svg
new file mode 100644
index 00000000..87b06da4
--- /dev/null
+++ b/clients/ui/frontend/src/images/empty-state-model-registries.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
diff --git a/clients/ui/frontend/src/images/favicon.ico b/clients/ui/frontend/src/images/favicon.ico
new file mode 100644
index 00000000..5351309a
Binary files /dev/null and b/clients/ui/frontend/src/images/favicon.ico differ
diff --git a/clients/ui/frontend/src/images/logo.svg b/clients/ui/frontend/src/images/logo.svg
new file mode 100644
index 00000000..fb06122e
--- /dev/null
+++ b/clients/ui/frontend/src/images/logo.svg
@@ -0,0 +1,43 @@
+
+
diff --git a/clients/ui/frontend/src/index.html b/clients/ui/frontend/src/index.html
index 5138a747..5abb9a45 100644
--- a/clients/ui/frontend/src/index.html
+++ b/clients/ui/frontend/src/index.html
@@ -3,10 +3,10 @@
- Model Registry UI
-
+ Model Registry
+
-
+