feat: add environment configuration files for frontend (#536)

Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
This commit is contained in:
Guilherme Caponetto 2025-08-26 11:37:20 -03:00 committed by Bhakti Narvekar
parent 42ffd9b0c5
commit e666e2ebfb
40 changed files with 6328 additions and 4505 deletions

4
workspaces/frontend/.env Normal file
View File

@ -0,0 +1,4 @@
LOGO=logo-light-theme.svg
LOGO_DARK=logo-dark-theme.svg
FAVICON=favicon.ico
PRODUCT_NAME="Notebooks"

View File

@ -1,2 +1,7 @@
# Test against prod build hosted by lightweight http server
BASE_URL=http://localhost:9001
DEPLOYMENT_MODE=standalone
POLL_INTERVAL=9999999
DIST_DIR=./dist
URL_PREFIX=/
PUBLIC_PATH=/

View File

@ -0,0 +1,3 @@
APP_ENV=development
DEPLOYMENT_MODE=standalone
MOCK_API_ENABLED=true

View File

@ -0,0 +1 @@
APP_ENV=production

View File

@ -5,6 +5,5 @@ yarn.lock
stats.json
coverage
.idea
.env
.vscode/*
!.vscode/settings.json

View File

@ -50,11 +50,7 @@ This is the default setup for running the UI locally. Make sure you build the pr
npm run start:dev
```
The command above requires the backend to be active in order to serve data. To run the UI independently, without establishing a connection to the backend, use the following command to start the application with a mocked API:
```bash
npm run start:dev:mock
```
The command above starts the UI with mocked data by default, so you can run the application without requiring a connection to the backend. This behavior can be customized in the `.env.development` file by setting the `MOCK_API_ENABLED` environment variable to `false`.
### Testing

View File

@ -0,0 +1,16 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
chrome: 110,
},
useBuiltIns: 'usage',
corejs: '3',
},
],
'@babel/preset-react',
'@babel/preset-typescript',
],
};

View File

@ -6,3 +6,5 @@ millicores
workspacekind
workspacekinds
healthcheck
pficon
svgs

View File

@ -0,0 +1,191 @@
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
const dotenvExpand = require('dotenv-expand');
const Dotenv = require('dotenv-webpack');
/**
* Determine if the project is standalone or nested.
*
* @param {string} directory
* @returns {boolean}
*/
const getProjectIsRootDir = (directory) => {
const dotenvLocalFile = path.resolve(directory, '.env.local');
const dotenvFile = path.resolve(directory, '.env');
let localIsRoot;
let isRoot;
if (fs.existsSync(dotenvLocalFile)) {
const { IS_PROJECT_ROOT_DIR: DOTENV_LOCAL_ROOT } = dotenv.parse(
fs.readFileSync(dotenvLocalFile),
);
localIsRoot = DOTENV_LOCAL_ROOT;
}
if (fs.existsSync(dotenvFile)) {
const { IS_PROJECT_ROOT_DIR: DOTENV_ROOT } = dotenv.parse(fs.readFileSync(dotenvFile));
isRoot = DOTENV_ROOT;
}
return localIsRoot !== undefined ? localIsRoot !== 'false' : isRoot !== 'false';
};
/**
* Return tsconfig compilerOptions.
*
* @param {string} directory
* @returns {object}
*/
const getTsCompilerOptions = (directory) => {
const tsconfigFile = path.resolve(directory, './tsconfig.json');
let tsCompilerOptions = {};
if (fs.existsSync(tsconfigFile)) {
const { compilerOptions = { outDir: './dist', baseUrl: './src' } } = require(tsconfigFile);
tsCompilerOptions = compilerOptions;
}
return tsCompilerOptions;
};
/**
* Setup a webpack dotenv plugin config.
*
* @param {string} path
* @returns {*}
*/
const setupWebpackDotenvFile = (path) => {
const settings = {
systemvars: true,
silent: true,
};
if (path) {
settings.path = path;
}
return new Dotenv(settings);
};
/**
* Setup multiple webpack dotenv file parameters.
*
* @param {string} directory
* @param {string} env
* @param {boolean} isRoot
* @returns {Array}
*/
const setupWebpackDotenvFilesForEnv = ({ directory, env, isRoot = true }) => {
const dotenvWebpackSettings = [];
if (process.env.CY_MOCK) {
dotenvWebpackSettings.push(
setupWebpackDotenvFile(path.resolve(directory, `.env.cypress.mock`)),
);
}
if (env) {
dotenvWebpackSettings.push(
setupWebpackDotenvFile(path.resolve(directory, `.env.${env}.local`)),
);
dotenvWebpackSettings.push(setupWebpackDotenvFile(path.resolve(directory, `.env.${env}`)));
}
dotenvWebpackSettings.push(setupWebpackDotenvFile(path.resolve(directory, '.env.local')));
dotenvWebpackSettings.push(setupWebpackDotenvFile(path.resolve(directory, '.env')));
if (!isRoot) {
if (env) {
dotenvWebpackSettings.push(
setupWebpackDotenvFile(path.resolve(directory, '..', `.env.${env}.local`)),
);
dotenvWebpackSettings.push(
setupWebpackDotenvFile(path.resolve(directory, '..', `.env.${env}`)),
);
}
dotenvWebpackSettings.push(setupWebpackDotenvFile(path.resolve(directory, '..', '.env.local')));
dotenvWebpackSettings.push(setupWebpackDotenvFile(path.resolve(directory, '..', '.env')));
}
return dotenvWebpackSettings;
};
/**
* Setup, and access, a dotenv file and the related set of parameters.
*
* @param {string} path
* @returns {*}
*/
const setupDotenvFile = (path) => {
const dotenvInitial = dotenv.config({ path });
dotenvExpand(dotenvInitial);
};
/**
* Setup and access local and specific dotenv file parameters.
*
* @param {string} env
*/
const setupDotenvFilesForEnv = ({ env }) => {
const RELATIVE_DIRNAME = path.resolve(__dirname, '..');
const IS_ROOT = getProjectIsRootDir(RELATIVE_DIRNAME);
const { baseUrl: TS_BASE_URL, outDir: TS_OUT_DIR } = getTsCompilerOptions(RELATIVE_DIRNAME);
if (process.env.CY_MOCK) {
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, `.env.cypress.mock`));
}
if (!IS_ROOT) {
if (env) {
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '..', `.env.${env}.local`));
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '..', `.env.${env}`));
}
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '..', '.env.local'));
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '..', '.env'));
}
if (env) {
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, `.env.${env}.local`));
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, `.env.${env}`));
}
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '.env.local'));
setupDotenvFile(path.resolve(RELATIVE_DIRNAME, '.env'));
const DEPLOYMENT_MODE = process.env.DEPLOYMENT_MODE || 'kubeflow';
const AUTH_METHOD = process.env.AUTH_METHOD || 'internal';
const IMAGES_DIRNAME = process.env.IMAGES_DIRNAME || 'images';
const PUBLIC_PATH = process.env.PUBLIC_PATH || '/workspaces';
const SRC_DIR = path.resolve(RELATIVE_DIRNAME, process.env.SRC_DIR || TS_BASE_URL || 'src');
const COMMON_DIR = path.resolve(RELATIVE_DIRNAME, process.env.COMMON_DIR || '../common');
const DIST_DIR = path.resolve(RELATIVE_DIRNAME, process.env.DIST_DIR || TS_OUT_DIR || 'dist');
const HOST = process.env.HOST || DEPLOYMENT_MODE === 'kubeflow' ? '0.0.0.0' : 'localhost';
const PORT = process.env.PORT || '9000';
const PROXY_PROTOCOL = process.env.PROXY_PROTOCOL || 'http';
const PROXY_HOST = process.env.PROXY_HOST || 'localhost';
const PROXY_PORT = process.env.PROXY_PORT || process.env.PORT || 4000;
const DEV_MODE = process.env.DEV_MODE || undefined;
const OUTPUT_ONLY = process.env._OUTPUT_ONLY === 'true';
process.env._RELATIVE_DIRNAME = RELATIVE_DIRNAME;
process.env._IS_PROJECT_ROOT_DIR = IS_ROOT;
process.env._IMAGES_DIRNAME = IMAGES_DIRNAME;
process.env._PUBLIC_PATH = PUBLIC_PATH;
process.env._SRC_DIR = SRC_DIR;
process.env._COMMON_DIR = COMMON_DIR;
process.env._DIST_DIR = DIST_DIR;
process.env._HOST = HOST;
process.env._PORT = PORT;
process.env._PROXY_PROTOCOL = PROXY_PROTOCOL;
process.env._PROXY_HOST = PROXY_HOST;
process.env._PROXY_PORT = PROXY_PORT;
process.env._OUTPUT_ONLY = OUTPUT_ONLY;
process.env._DEV_MODE = DEV_MODE;
process.env._DEPLOYMENT_MODE = DEPLOYMENT_MODE;
process.env._AUTH_METHOD = AUTH_METHOD;
};
module.exports = { setupWebpackDotenvFilesForEnv, setupDotenvFilesForEnv };

View File

@ -23,5 +23,6 @@ module.exports = {
relativeDir,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css',
),
path.resolve(relativeDir, 'node_modules/@patternfly/react-catalog-view-extension/dist/css'),
],
};

View File

@ -1,47 +1,74 @@
/* 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 ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { EnvironmentPlugin } = require('webpack');
const APP_PREFIX = process.env.APP_PREFIX || '/workspaces';
const IMAGES_DIRNAME = 'images';
const relativeDir = path.resolve(__dirname, '..');
module.exports = (env) => {
return {
const { setupWebpackDotenvFilesForEnv } = require('./dotenv');
const { name } = require('../package.json');
const RELATIVE_DIRNAME = process.env._RELATIVE_DIRNAME;
const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
const IMAGES_DIRNAME = process.env._IMAGES_DIRNAME;
const PUBLIC_PATH = process.env._PUBLIC_PATH;
const SRC_DIR = process.env._SRC_DIR;
const COMMON_DIR = process.env._COMMON_DIR;
const DIST_DIR = process.env._DIST_DIR;
const OUTPUT_ONLY = process.env._OUTPUT_ONLY;
const FAVICON = process.env.FAVICON;
const PRODUCT_NAME = process.env.PRODUCT_NAME;
const COVERAGE = process.env.COVERAGE;
const DEPLOYMENT_MODE = process.env._DEPLOYMENT_MODE;
const BASE_PATH = DEPLOYMENT_MODE === 'kubeflow' ? '/workspaces' : PUBLIC_PATH;
if (OUTPUT_ONLY !== 'true') {
console.info(
`\nPrepping files...` +
`\n\tSRC DIR: ${SRC_DIR}` +
`\n\tOUTPUT DIR: ${DIST_DIR}` +
`\n\tPUBLIC PATH: ${PUBLIC_PATH}` +
`\n\tBASE_PATH: ${BASE_PATH}\n`,
);
if (COVERAGE === 'true') {
console.info('\nAdding code coverage instrumentation.\n');
}
}
module.exports = (env) => ({
entry: {
app: path.join(SRC_DIR, 'index.tsx'),
},
module: {
rules: [
{
test: /\.(tsx|ts|jsx)?$/,
test: /\.(tsx|ts|jsx|js)?$/,
exclude: [/node_modules/, /__tests__/, /__mocks__/],
include: [SRC_DIR, COMMON_DIR],
use: [
{
COVERAGE === 'true' && '@jsdevtools/coverage-istanbul-loader',
env === 'development'
? { loader: 'swc-loader' }
: {
loader: 'ts-loader',
options: {
experimentalWatchApi: true,
transpileOnly: 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(relativeDir, 'node_modules/patternfly/dist/fonts'),
path.resolve(RELATIVE_DIRNAME, 'node_modules/patternfly/dist/fonts'),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/dist/styles/assets/fonts',
),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'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'),
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly/patternfly/assets/fonts'),
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly/patternfly/assets/pficon'),
],
use: {
loader: 'file-loader',
@ -96,30 +123,31 @@ module.exports = (env) => {
{
test: /\.(jpg|jpeg|png|gif)$/i,
include: [
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'),
SRC_DIR,
COMMON_DIR,
path.resolve(RELATIVE_DIRNAME, 'node_modules/patternfly'),
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly/patternfly/assets/images'),
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly/react-styles/css/assets/images'),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/dist/styles/assets/images',
),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
relativeDir,
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images',
),
],
type: 'asset/inline',
use: [
{
loader: 'url-loader',
options: {
limit: 5000,
outputPath: 'images',
@ -140,47 +168,78 @@ module.exports = (env) => {
],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
include: [
path.resolve(
relativeDir,
'node_modules/@patternfly/react-catalog-view-extension/dist/css/react-catalog-view-extension.css',
),
],
test: /\.ya?ml$/,
use: 'js-yaml-loader',
},
],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(relativeDir, 'dist'),
publicPath: APP_PREFIX,
path: DIST_DIR,
publicPath: BASE_PATH,
uniqueName: name,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(relativeDir, 'src', 'index.html'),
...setupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
isRoot: IS_PROJECT_ROOT_DIR,
}),
new Dotenv({
systemvars: true,
silent: true,
new HtmlWebpackPlugin({
template: path.join(SRC_DIR, 'index.html'),
title: PRODUCT_NAME,
favicon: path.join(SRC_DIR, 'images', FAVICON),
publicPath: BASE_PATH,
base: {
href: BASE_PATH,
},
chunks: ['app'],
}),
new CopyPlugin({
patterns: [{ from: './src/images', to: 'images' }],
}),
new ForkTsCheckerWebpackPlugin(),
new EnvironmentPlugin({
APP_PREFIX: process.env.APP_PREFIX || '/workspaces',
patterns: [
{
from: path.join(SRC_DIR, 'locales'),
to: path.join(DIST_DIR, 'locales'),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'favicons'),
to: path.join(DIST_DIR, 'favicons'),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'images'),
to: path.join(DIST_DIR, 'images'),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'favicon.ico'),
to: path.join(DIST_DIR),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'favicon.png'),
to: path.join(DIST_DIR),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'manifest.json'),
to: path.join(DIST_DIR),
noErrorOnMissing: true,
},
{
from: path.join(SRC_DIR, 'robots.txt'),
to: path.join(DIST_DIR),
noErrorOnMissing: true,
},
],
}),
],
resolve: {
extensions: ['.js', '.ts', '.tsx', '.jsx'],
plugins: [
new TsconfigPathsPlugin({
configFile: path.resolve(relativeDir, './tsconfig.json'),
}),
],
alias: {
'~': path.resolve(SRC_DIR),
},
symlinks: false,
cacheWithContext: false,
},
};
};
});

View File

@ -1,60 +1,141 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { execSync } = require('child_process');
const path = require('path');
const { EnvironmentPlugin } = require('webpack');
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';
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 MOCK_API_ENABLED = process.env.MOCK_API_ENABLED || 'false';
const relativeDir = path.resolve(__dirname, '..');
const APP_PREFIX = process.env.APP_PREFIX || '/workspaces';
const { setupWebpackDotenvFilesForEnv, setupDotenvFilesForEnv } = require('./dotenv');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
module.exports = merge(common('development'), {
const smp = new SpeedMeasurePlugin({ disable: !process.env.MEASURE });
setupDotenvFilesForEnv({ env: 'development' });
const webpackCommon = require('./webpack.common.js');
const RELATIVE_DIRNAME = process.env._RELATIVE_DIRNAME;
const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
const SRC_DIR = process.env._SRC_DIR;
const COMMON_DIR = process.env._COMMON_DIR;
const PUBLIC_PATH = process.env._PUBLIC_PATH;
const DIST_DIR = process.env._DIST_DIR;
const HOST = process.env._HOST;
const PORT = process.env._PORT;
const PROXY_PROTOCOL = process.env._PROXY_PROTOCOL;
const PROXY_HOST = process.env._PROXY_HOST;
const PROXY_PORT = process.env._PROXY_PORT;
const DEPLOYMENT_MODE = process.env._DEPLOYMENT_MODE;
const AUTH_METHOD = process.env._AUTH_METHOD;
const BASE_PATH = DEPLOYMENT_MODE === 'kubeflow' ? '/workspaces' : PUBLIC_PATH;
// Function to generate headers based on deployment mode
const getProxyHeaders = () => {
if (AUTH_METHOD === 'internal') {
return {
'kubeflow-userid': 'user@example.com',
};
}
if (AUTH_METHOD === 'user_token') {
try {
const token = execSync(
"kubectl config view --raw --minify --flatten -o jsonpath='{.users[].user.token}'",
)
.toString()
.trim();
const username = execSync("kubectl auth whoami -o jsonpath='{.status.userInfo.username}'")
.toString()
.trim();
// eslint-disable-next-line no-console
console.info('Logged in as user:', username);
return {
Authorization: `Bearer ${token}`,
'x-forwarded-access-token': token,
};
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to get Kubernetes token:', error.message);
return {};
}
}
return {};
};
module.exports = smp.wrap(
merge(
{
plugins: [
...setupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
env: 'development',
isRoot: IS_PROJECT_ROOT_DIR,
}),
],
},
webpackCommon('development'),
{
mode: 'development',
devtool: 'eval-source-map',
optimization: {
runtimeChunk: 'single',
removeEmptyChunks: true,
},
devServer: {
host: HOST,
port: PORT,
compress: true,
historyApiFallback: {
index: APP_PREFIX + '/index.html',
},
open: [APP_PREFIX],
static: {
directory: path.resolve(relativeDir, 'dist'),
publicPath: APP_PREFIX,
},
client: {
overlay: true,
index: `${BASE_PATH}/index.html`,
},
hot: true,
open: [BASE_PATH],
proxy: [
{
context: ['/api'],
context: ['/api', '/workspaces/api'],
target: {
host: PROXY_HOST,
protocol: PROXY_PROTOCOL,
port: PROXY_PORT,
},
changeOrigin: true,
headers: getProxyHeaders(),
},
],
devMiddleware: {
stats: 'errors-only',
},
client: {
overlay: false,
},
static: {
directory: DIST_DIR,
publicPath: BASE_PATH,
},
onListening: (devServer) => {
if (devServer) {
// eslint-disable-next-line no-console
console.log(
`\x1b[32m✓ Dashboard available at: \x1b[4mhttp://localhost:${
devServer.server.address().port
}\x1b[0m`,
);
}
},
},
module: {
rules: [
{
test: /\.css$/,
include: [...stylePaths],
include: [
SRC_DIR,
COMMON_DIR,
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly'),
],
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new EnvironmentPlugin({
WEBPACK_REPLACE__mockApiEnabled: MOCK_API_ENABLED,
}),
new ForkTsCheckerWebpackPlugin(),
new ReactRefreshWebpackPlugin({ overlay: false }),
],
});
},
),
);

View File

@ -1,44 +1,61 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const { EnvironmentPlugin } = require('webpack');
const { stylePaths } = require('./stylePaths');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const { setupWebpackDotenvFilesForEnv, setupDotenvFilesForEnv } = require('./dotenv');
const PRODUCTION = process.env.PRODUCTION || 'false';
setupDotenvFilesForEnv({ env: 'production' });
const webpackCommon = require('./webpack.common.js');
module.exports = merge(common('production'), {
const RELATIVE_DIRNAME = process.env._RELATIVE_DIRNAME;
const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
const SRC_DIR = process.env._SRC_DIR;
const COMMON_DIR = process.env._COMMON_DIR;
const DIST_DIR = process.env._DIST_DIR;
const OUTPUT_ONLY = process.env._OUTPUT_ONLY;
if (OUTPUT_ONLY !== 'true') {
console.info(`Cleaning OUTPUT DIR...\n ${DIST_DIR}\n`);
}
module.exports = merge(
{
plugins: [
...setupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
env: 'production',
isRoot: IS_PROJECT_ROOT_DIR,
}),
],
},
webpackCommon('production'),
{
mode: 'production',
devtool: 'source-map',
optimization: {
minimizer: [
new TerserJSPlugin({}),
new CssMinimizerPlugin({
minimizerOptions: {
preset: ['default', { mergeLonghand: false }],
},
}),
],
minimize: true,
minimizer: [new TerserJSPlugin(), new CssMinimizerPlugin()],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].bundle.css',
}),
new EnvironmentPlugin({
PRODUCTION,
ignoreOrder: true,
}),
],
module: {
rules: [
{
test: /\.css$/,
include: [...stylePaths],
include: [
SRC_DIR,
COMMON_DIR,
path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly'),
],
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
});
},
);

File diff suppressed because it is too large Load Diff

View File

@ -18,91 +18,88 @@
"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": "cross-env PRODUCTION=true webpack --config ./config/webpack.prod.js",
"build:prod": "webpack --config ./config/webpack.prod.js",
"generate:api": "./scripts/generate-api.sh && npm run prettier",
"start:dev": "cross-env STYLE_THEME=$npm_config_theme webpack serve --hot --color --config ./config/webpack.dev.js",
"start:dev:mock": "cross-env MOCK_API_ENABLED=true STYLE_THEME=$npm_config_theme npm run start:dev",
"test": "run-s prettier:check test:lint test:unit test:cypress-ci",
"test:cypress-ci": "npx concurrently -P -k -s first \"npm run cypress:server:build && npm run cypress:server\" \"npx wait-on tcp:127.0.0.1:9001 && npm run cypress:run:mock -- {@}\" -- ",
"start:dev": "webpack serve --hot --color --config ./config/webpack.dev.js",
"test": "run-s prettier:check test:lint test:type-check test:unit test:cypress-ci",
"test:cypress-ci": "npx concurrently -P -k -s first \"CY_MOCK=1 npm run cypress:server:build && npm run cypress:server\" \"npx wait-on tcp:127.0.0.1:9001 && npm run cypress:run:mock -- {@}\" -- ",
"test:jest": "jest --passWithNoTests",
"test:unit": "npm run test:jest -- --silent",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:lint": "eslint --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src",
"test:lint:fix": "eslint --ext .js,.ts,.jsx,.tsx ./src --fix",
"test:type-check": "tsc --noEmit",
"test:fix": "run-s prettier test:lint:fix",
"cypress:open": "cypress open --project src/__tests__/cypress",
"cypress:open:mock": "CY_MOCK=1 CY_WS_PORT=9002 npm run cypress:open -- ",
"cypress:run": "cypress run -b chrome --project src/__tests__/cypress",
"cypress:run:mock": "CY_MOCK=1 npm run cypress:run -- ",
"cypress:server:build": "POLL_INTERVAL=9999999 FAST_POLL_INTERVAL=9999999 APP_PREFIX=/ webpack --config ./config/webpack.prod.js",
"cypress:server:build": "npm run build",
"cypress:server": "serve ./dist -p 9001 -s -L",
"prettier": "prettier --ignore-path .gitignore --write \"**/*{.ts,.tsx,.js,.cjs,.jsx,.css,.json}\"",
"prettier:check": "prettier --ignore-path .gitignore --check \"**/*{.ts,.tsx,.js,.cjs,.jsx,.css,.json}\"",
"prepare": "cd ../../ && husky workspaces/frontend/.husky"
},
"devDependencies": {
"@cspell/eslint-plugin": "^9.1.2",
"@cypress/code-coverage": "^3.13.5",
"@mui/icons-material": "^6.3.1",
"@mui/icons-material": "^6.4.8",
"@mui/material": "^6.3.1",
"@mui/types": "^7.2.21",
"@testing-library/cypress": "^10.0.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "14.4.3",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@swc/core": "^1.9.1",
"@types/chai-subset": "^1.3.5",
"@types/jest": "^29.5.3",
"@types/react-router-dom": "^5.3.3",
"@types/victory": "^33.1.5",
"@types/classnames": "^2.3.1",
"@types/dompurify": "^3.2.0",
"@types/jest": "^29.5.13",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.8",
"@types/react-dom": "^18.3.1",
"@types/showdown": "^2.0.3",
"@typescript-eslint/eslint-plugin": "^8.35.0",
"chai-subset": "^1.6.0",
"concurrently": "^9.1.0",
"copy-webpack-plugin": "^11.0.0",
"core-js": "^3.39.0",
"cross-env": "^7.0.3",
"css-loader": "^6.11.0",
"css-minimizer-webpack-plugin": "^5.0.1",
"cypress": "^13.16.1",
"cypress-axe": "^1.5.0",
"cypress-high-resolution": "^1.0.0",
"cypress-mochawesome-reporter": "^3.8.2",
"cypress-multi-reporters": "^2.0.4",
"dotenv": "^16.4.5",
"dotenv-webpack": "^8.1.0",
"expect": "^29.7.0",
"fork-ts-checker-webpack-plugin": "^9.0.3",
"html-webpack-plugin": "^5.6.0",
"copy-webpack-plugin": "^13.0.0",
"core-js": "^3.40.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.0",
"dotenv": "^16.5.0",
"dotenv-expand": "^5.1.0",
"dotenv-webpack": "^6.0.0",
"expect": "^30.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^9.0.2",
"html-webpack-plugin": "^5.6.3",
"husky": "^9.1.7",
"imagemin": "^8.0.1",
"imagemin": "^9.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"junit-report-merger": "^7.0.0",
"junit-report-merger": "^7.0.1",
"mini-css-extract-plugin": "^2.9.0",
"postcss": "^8.4.38",
"prettier": "^3.3.0",
"postcss": "^8.4.49",
"prettier": "^3.3.3",
"prop-types": "^15.8.1",
"raw-loader": "^4.0.2",
"react-router-dom": "^6.26.1",
"regenerator-runtime": "^0.13.11",
"react-refresh": "^0.14.2",
"regenerator-runtime": "^0.14.1",
"rimraf": "^6.0.1",
"sass": "^1.83.4",
"sass-loader": "^16.0.4",
"serve": "^14.2.1",
"style-loader": "^3.3.4",
"sass": "^1.87.0",
"sass-loader": "^16.0.0",
"speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^4.0.0",
"svg-url-loader": "^8.0.0",
"terser-webpack-plugin": "^5.3.10",
"ts-jest": "^29.1.4",
"ts-loader": "^9.5.1",
"swagger-typescript-api": "13.2.7",
"swc-loader": "^0.2.6",
"terser-webpack-plugin": "^5.3.11",
"ts-loader": "^9.5.2",
"tsconfig-paths-webpack-plugin": "^4.1.0",
"tslib": "^2.6.0",
"typescript": "^5.4.5",
"tslib": "^2.7.0",
"typescript": "^5.8.2",
"url-loader": "^4.1.1",
"webpack": "^5.91.0",
"webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.2.2",
"webpack-merge": "^5.10.0"
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0",
"webpack-merge": "^6.0.1"
},
"dependencies": {
"@patternfly/patternfly": "^6.3.1",
@ -112,35 +109,58 @@
"@patternfly/react-icons": "^6.3.1",
"@patternfly/react-styles": "^6.3.1",
"@patternfly/react-table": "^6.3.1",
"@patternfly/react-templates": "^6.3.1",
"@patternfly/react-tokens": "^6.3.1",
"@types/js-yaml": "^4.0.9",
"axios": "^1.10.0",
"date-fns": "^4.1.0",
"eslint-plugin-local-rules": "^3.0.2",
"js-yaml": "^4.1.0",
"npm-run-all": "^4.1.5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6.26.2",
"sirv-cli": "^2.0.2"
},
"optionalDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.12.2",
"axios": "^1.10.0",
"date-fns": "^4.1.0",
"classnames": "^2.2.6",
"dompurify": "^3.2.4",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.15",
"react": "^18",
"react-dom": "^18",
"react-router": "^7.5.2",
"react-router-dom": "^7.6.1",
"sass": "^1.83.0",
"showdown": "^2.1.0"
},
"optionalDependencies": {
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.5",
"@cspell/eslint-plugin": "^9.1.2",
"@cypress/code-coverage": "^3.14.1",
"@cypress/webpack-preprocessor": "^6.0.4",
"@testing-library/cypress": "^10.0.3",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "14.6.1",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"cypress": "^14.4.1",
"cypress-axe": "^1.6.0",
"cypress-high-resolution": "^1.0.0",
"cypress-mochawesome-reporter": "^3.8.2",
"cypress-multi-reporters": "^2.0.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-node": "^0.3.7",
"eslint-import-resolver-typescript": "^3.5.3",
"eslint-import-resolver-typescript": "^3.8.3",
"eslint-plugin-cypress": "^3.3.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-local-rules": "^3.0.2",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-no-relative-import-paths": "^1.5.2",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"swagger-typescript-api": "^13.2.7"
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"npm-run-all": "^4.1.5",
"serve": "^14.2.4",
"ts-jest": "^29.4.0"
}
}

View File

@ -4,12 +4,16 @@ import { defineConfig } from 'cypress';
import coverage from '@cypress/code-coverage/task';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available
import webpack from '@cypress/webpack-preprocessor';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available
import cypressHighResolution from 'cypress-high-resolution';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available
import { beforeRunHook, afterRunHook } from 'cypress-mochawesome-reporter/lib';
import { mergeFiles } from 'junit-report-merger';
import { env, BASE_URL } from '~/__tests__/cypress/cypress/utils/testConfig';
import webpackConfig from './cypress/webpack.config';
const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`;
@ -41,7 +45,6 @@ export default defineConfig({
env: {
MOCK: !!env.CY_MOCK,
coverage: !!env.CY_COVERAGE,
APP_PREFIX: env.APP_PREFIX || '/workspaces',
codeCoverage: {
exclude: [path.resolve(__dirname, '../../third_party/**')],
},
@ -49,12 +52,20 @@ export default defineConfig({
},
defaultCommandTimeout: 10000,
e2e: {
baseUrl: env.CY_MOCK ? BASE_URL || 'http://localhost:9001' : 'http://localhost:9000',
baseUrl: BASE_URL,
specPattern: env.CY_MOCK ? `cypress/tests/mocked/**/*.cy.ts` : `cypress/tests/e2e/**/*.cy.ts`,
experimentalInteractiveRunEvents: true,
setupNodeEvents(on, config) {
cypressHighResolution(on, config);
coverage(on, config);
// Configure webpack preprocessor with custom webpack config
const options = {
webpackOptions: webpackConfig,
watchOptions: {},
};
on('file:preprocessor', webpack(options));
on('task', {
readJSON(filePath: string) {
const absPath = path.resolve(__dirname, filePath);

View File

@ -13,7 +13,7 @@ describe('Application', () => {
cy.intercept('GET', `/api/v1/workspaces/${mockNamespaces[0].name}`, {
body: mockBFFResponse({ mockWorkspace1 }),
}).as('getWorkspaces');
cy.visit('/workspaces');
cy.visit('/');
cy.wait('@getNamespaces');
cy.wait('@getWorkspaces');
});

View File

@ -6,7 +6,7 @@ describe('WorkspaceDetailsActivity Component', () => {
cy.intercept('GET', 'api/v1/workspaces', {
body: mockBFFResponse(mockWorkspaces),
}).as('getWorkspaces');
cy.visit('/workspaces');
cy.visit('/');
});
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE

View File

@ -0,0 +1,115 @@
import path from 'path';
const webpackConfig = {
mode: 'development' as const,
resolve: {
extensions: ['.ts', '.tsx', '.js', '.jsx'],
alias: {
'~': path.resolve(__dirname, '../../../'),
},
},
module: {
rules: [
{
test: /\.(tsx|ts|jsx|js)?$/,
exclude: [/node_modules/],
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
},
},
],
},
{
test: /\.(svg|ttf|eot|woff|woff2)$/,
// Handle fonts from PatternFly and other sources
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'),
],
use: {
loader: 'file-loader',
options: {
limit: 5000,
outputPath: 'fonts',
name: '[name].[ext]',
},
},
},
{
test: /\.svg$/,
include: (input: string): boolean => input.indexOf('background-filter.svg') > 1,
use: [
{
loader: 'url-loader',
options: {
limit: 5000,
outputPath: 'svgs',
name: '[name].[ext]',
},
},
],
},
{
test: /\.svg$/,
// Handle SVG files
include: (input: string): boolean =>
input.indexOf('images') > -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, '../../'),
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',
),
],
use: [
{
loader: 'url-loader',
options: {
limit: 5000,
outputPath: 'images',
name: '[name].[ext]',
},
},
],
},
{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
};
export default webpackConfig;

View File

@ -1,4 +1,4 @@
import { TextEncoder } from 'util';
import { TextEncoder as UtilTextEncoder } from 'util';
import { JestAssertionError } from 'expect';
import 'core-js/actual/array/to-sorted';
import {
@ -7,7 +7,10 @@ import {
createComparativeValue,
} from '~/__tests__/unit/testUtils/hooks';
global.TextEncoder = TextEncoder;
// Ensure TextEncoder is available in the JSDOM environment for tests.
// Node's util.TextEncoder has slightly different TS types than DOM's, so cast to any.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).TextEncoder = UtilTextEncoder;
const tryExpect = (expectFn: () => void) => {
try {

View File

@ -22,14 +22,15 @@ import { BarsIcon } from '@patternfly/react-icons/dist/esm/icons/bars-icon';
import ErrorBoundary from '~/app/error/ErrorBoundary';
import NamespaceSelector from '~/shared/components/NamespaceSelector';
import logoDarkTheme from '~/images/logo-dark-theme.svg';
import { DEPLOYMENT_MODE, isMUITheme } from '~/shared/utilities/const';
import { DeploymentMode, Theme } from '~/shared/utilities/types';
import { NamespaceContextProvider } from './context/NamespaceContextProvider';
import AppRoutes from './AppRoutes';
import NavSidebar from './NavSidebar';
import { NotebookContextProvider } from './context/NotebookContext';
import { isMUITheme, Theme } from './const';
import { BrowserStorageContextProvider } from './context/BrowserStorageContext';
const isStandalone = process.env.PRODUCTION !== 'true';
const isStandalone = DEPLOYMENT_MODE === DeploymentMode.Standalone;
const App: React.FC = () => {
useEffect(() => {

View File

@ -69,7 +69,10 @@ const AppRoutes: React.FC = () => {
<Route path={AppRoutePaths.workspaceKinds} element={<WorkspaceKinds />} />
<Route path={AppRoutePaths.workspaceKindCreate} element={<WorkspaceKindForm />} />
<Route path={AppRoutePaths.workspaceKindEdit} element={<WorkspaceKindForm />} />
<Route path="/" element={<Navigate to={AppRoutePaths.workspaces} replace />} />
<Route
path={AppRoutePaths.root}
element={<Navigate to={AppRoutePaths.workspaces} replace />}
/>
<Route path="*" element={<NotFound />} />
{
// TODO: Remove the linter skip when we implement authentication

View File

@ -9,8 +9,8 @@ import {
} from '@patternfly/react-core/dist/esm/components/Nav';
import { PageSidebar, PageSidebarBody } from '@patternfly/react-core/dist/esm/components/Page';
import { useTypedLocation } from '~/app/routerHelper';
import { isMUITheme, LOGO_LIGHT, URL_PREFIX } from '~/shared/utilities/const';
import { useNavData, isNavDataGroup, NavDataHref, NavDataGroup } from './AppRoutes';
import { APP_PREFIX, isMUITheme, LOGO_LIGHT } from './const';
const NavHref: React.FC<{ item: NavDataHref }> = ({ item }) => {
const location = useTypedLocation();
@ -61,7 +61,7 @@ const NavSidebar: React.FC = () => {
<NavItem>
<Brand
className="kubeflow_brand"
src={`${window.location.origin}${APP_PREFIX}/images/${LOGO_LIGHT}`}
src={`${window.location.origin}${URL_PREFIX}/images/${LOGO_LIGHT}`}
alt="Kubeflow Logo"
/>
</NavItem>

View File

@ -4,8 +4,8 @@ import {
SearchInputProps,
} from '@patternfly/react-core/dist/esm/components/SearchInput';
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import FormFieldset from 'app/components/FormFieldset';
import { isMUITheme } from 'app/const';
import FormFieldset from '~/app/components/FormFieldset';
import { isMUITheme } from '~/shared/utilities/const';
type ThemeAwareSearchInputProps = Omit<SearchInputProps, 'onChange' | 'onClear'> & {
onChange: (value: string) => void; // Simplified onChange signature

View File

@ -504,12 +504,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
})}
{canCreateWorkspaces && (
<ToolbarItem>
<Button
size="lg"
variant="primary"
ouiaId="Primary"
onClick={createWorkspace}
>
<Button variant="primary" ouiaId="Primary" onClick={createWorkspace}>
Create workspace
</Button>
</ToolbarItem>

View File

@ -1,16 +0,0 @@
export const BFF_API_VERSION = 'v1';
export enum Theme {
Default = 'default-theme',
MUI = 'mui-theme',
// Future themes can be added here
}
export const isMUITheme = (): boolean => STYLE_THEME === Theme.MUI;
const STYLE_THEME = process.env.STYLE_THEME || Theme.MUI;
export const LOGO_LIGHT = process.env.LOGO || 'logo.svg';
export const DEFAULT_POLLING_RATE_MS = 10000;
export const APP_PREFIX = process.env.APP_PREFIX || '/workspaces';

View File

@ -1,6 +1,6 @@
import React, { ReactNode, useMemo } from 'react';
import { APP_PREFIX, BFF_API_VERSION } from '~/app/const';
import EnsureAPIAvailability from '~/app/EnsureAPIAvailability';
import { BFF_API_PREFIX, BFF_API_VERSION } from '~/shared/utilities/const';
import useNotebookAPIState, { NotebookAPIState } from './useNotebookAPIState';
export type NotebookContextType = {
@ -19,8 +19,8 @@ interface NotebookContextProviderProps {
}
export const NotebookContextProvider: React.FC<NotebookContextProviderProps> = ({ children }) => {
// Remove trailing slash from APP_PREFIX to avoid double slashes
const cleanPrefix = APP_PREFIX.replace(/\/$/, '');
// Remove trailing slash from BFF_API_PREFIX to avoid double slashes
const cleanPrefix = BFF_API_PREFIX.replace(/\/$/, '');
const hostPath = `${cleanPrefix}/api/${BFF_API_VERSION}`;
const [apiState, refreshAPIState] = useNotebookAPIState(hostPath);

View File

@ -3,11 +3,10 @@ import { NotebookApis, notebookApisImpl } from '~/shared/api/notebookApi';
import { APIState } from '~/shared/api/types';
import useAPIState from '~/shared/api/useAPIState';
import { mockNotebookApisImpl } from '~/shared/mock/mockNotebookApis';
import { MOCK_API_ENABLED } from '~/shared/utilities/const';
export type NotebookAPIState = APIState<NotebookApis>;
const MOCK_API_ENABLED = process.env.WEBPACK_REPLACE__mockApiEnabled === 'true';
const useNotebookAPIState = (
hostPath: string | null,
): [apiState: NotebookAPIState, refreshAPIState: () => void] => {

View File

@ -15,8 +15,9 @@ import useGenericObjectState from '~/app/hooks/useGenericObjectState';
import { useNotebookAPI } from '~/app/hooks/useNotebookAPI';
import { WorkspaceKindFormData } from '~/app/types';
import { safeApiCall } from '~/shared/api/apiUtils';
import { CONTENT_TYPE_KEY, ContentType } from '~/shared/utilities/const';
import { CONTENT_TYPE_KEY } from '~/shared/utilities/const';
import { ApiValidationError, WorkspacekindsWorkspaceKind } from '~/generated/data-contracts';
import { ContentType } from '~/shared/utilities/types';
import { WorkspaceKindFileUpload } from './fileUpload/WorkspaceKindFileUpload';
import { WorkspaceKindFormProperties } from './properties/WorkspaceKindFormProperties';
import { WorkspaceKindFormImage } from './image/WorkspaceKindFormImage';

View File

@ -23,7 +23,7 @@ export const WorkspaceKindDetailsImages: React.FunctionComponent<WorkspaceDetail
workspaceCount:
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByImage[image.id] ?? 0
? (workspaceCountPerKind[workspaceKind.name].countByImage[image.id] ?? 0)
: 0,
}))}
tableKind="image"

View File

@ -18,7 +18,7 @@ export const WorkspaceKindDetailsPodConfigs: React.FunctionComponent<
kindName: workspaceKind.name,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
workspaceCount: workspaceCountPerKind[workspaceKind.name]
? workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0
? (workspaceCountPerKind[workspaceKind.name].countByPodConfig[podConfig.id] ?? 0)
: 0,
workspaceCountRouteState: {
podConfigId: podConfig.id,

View File

@ -8,11 +8,11 @@ import { useTypedLocation, useTypedNavigate, useTypedParams } from '~/app/router
import WorkspaceTable, { WorkspaceTableRef } from '~/app/components/WorkspaceTable';
import { useWorkspacesByKind } from '~/app/hooks/useWorkspaces';
import WorkspaceKindSummaryExpandableCard from '~/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard';
import { DEFAULT_POLLING_RATE_MS } from '~/app/const';
import { LoadingSpinner } from '~/app/components/LoadingSpinner';
import { LoadError } from '~/app/components/LoadError';
import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions';
import { usePolling } from '~/app/hooks/usePolling';
import { POLL_INTERVAL } from '~/shared/utilities/const';
const WorkspaceKindSummary: React.FC = () => {
const [isSummaryExpanded, setIsSummaryExpanded] = useState(true);
@ -30,7 +30,7 @@ const WorkspaceKindSummary: React.FC = () => {
podConfigId,
});
usePolling(refreshWorkspaces, DEFAULT_POLLING_RATE_MS);
usePolling(refreshWorkspaces, POLL_INTERVAL);
const tableRowActions = useWorkspaceRowActions([{ id: 'viewDetails' }]);

View File

@ -5,12 +5,12 @@ import { Stack, StackItem } from '@patternfly/react-core/dist/esm/layouts/Stack'
import WorkspaceTable from '~/app/components/WorkspaceTable';
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces';
import { DEFAULT_POLLING_RATE_MS } from '~/app/const';
import { LoadingSpinner } from '~/app/components/LoadingSpinner';
import { LoadError } from '~/app/components/LoadError';
import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions';
import { usePolling } from '~/app/hooks/usePolling';
import { WorkspacesWorkspaceState } from '~/generated/data-contracts';
import { POLL_INTERVAL } from '~/shared/utilities/const';
export const Workspaces: React.FunctionComponent = () => {
const { selectedNamespace } = useNamespaceContext();
@ -18,7 +18,7 @@ export const Workspaces: React.FunctionComponent = () => {
const [workspaces, workspacesLoaded, workspacesLoadError, refreshWorkspaces] =
useWorkspacesByNamespace(selectedNamespace);
usePolling(refreshWorkspaces, DEFAULT_POLLING_RATE_MS);
usePolling(refreshWorkspaces, POLL_INTERVAL);
const tableRowActions = useWorkspaceRowActions([
{ id: 'viewDetails' },

View File

@ -62,7 +62,7 @@ type NavigateOptions<T extends AppRouteKey> = CommonNavigateOptions &
* Go to my route
* </Link>
*/
export function buildPath<T extends AppRouteKey>(to: T, params: RouteParamsMap[T]): string {
export function buildPath<T extends AppRouteKey>(to: T, params?: RouteParamsMap[T]): string {
return generatePath(AppRoutePaths[to], params as RouteParamsMap[T]);
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,15 +2,15 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { URL_PREFIX } from '~/shared/utilities/const';
import App from './app/App';
import { APP_PREFIX } from './app/const';
const theme = createTheme({ cssVariables: true });
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<React.StrictMode>
<Router basename={APP_PREFIX}>
<Router basename={URL_PREFIX}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>

View File

@ -158,3 +158,21 @@ type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U]
* ```
*/
export type ExactlyOne<T> = AtMostOne<T> & AtLeastOne<T>;
export const asEnumMember = <T extends object>(
member: T[keyof T] | string | number | undefined | null,
e: T,
): T[keyof T] | null => (isEnumMember(member, e) ? member : null);
export const isEnumMember = <T extends object>(
member: T[keyof T] | string | number | undefined | unknown | null,
e: T,
): member is T[keyof T] => {
if (member != null) {
return Object.entries(e)
.filter(([key]) => Number.isNaN(Number(key)))
.map(([, value]) => value)
.includes(member);
}
return false;
};

View File

@ -1,8 +1,19 @@
import { asEnumMember } from '~/shared/typeHelpers';
import { DeploymentMode, Theme } from '~/shared/utilities/types';
export const STYLE_THEME = asEnumMember(process.env.STYLE_THEME, Theme) || Theme.MUI;
export const DEPLOYMENT_MODE =
asEnumMember(process.env.DEPLOYMENT_MODE, DeploymentMode) || DeploymentMode.Kubeflow;
export const DEV_MODE = process.env.APP_ENV === 'development';
export const AUTH_HEADER = process.env.AUTH_HEADER || 'kubeflow-userid';
export const POLL_INTERVAL = process.env.POLL_INTERVAL
? parseInt(process.env.POLL_INTERVAL)
: 30000;
export const LOGO_LIGHT = process.env.LOGO || 'logo-light-theme.svg';
export const URL_PREFIX = process.env.URL_PREFIX ?? '/workspaces';
export const BFF_API_PREFIX = process.env.BFF_API_PREFIX ?? '/';
export const BFF_API_VERSION = 'v1';
export const MOCK_API_ENABLED = process.env.MOCK_API_ENABLED === 'true';
export const CONTENT_TYPE_KEY = 'Content-Type';
export enum ContentType {
YAML = 'application/yaml',
}
export const isMUITheme = (): boolean => STYLE_THEME === Theme.MUI;

View File

@ -0,0 +1,14 @@
export enum DeploymentMode {
Standalone = 'standalone',
Kubeflow = 'kubeflow',
}
export enum Theme {
Default = 'default-theme',
MUI = 'mui-theme',
// Future themes can be added here
}
export enum ContentType {
YAML = 'application/yaml',
}

View File

@ -1,14 +1,13 @@
{
"compilerOptions": {
"baseUrl": "./src",
"rootDir": ".",
"outDir": "dist",
"module": "esnext",
"target": "ES6",
"lib": ["es6", "dom"],
"target": "es2021",
"lib": ["es2021", "dom", "ES2023.Array"],
"sourceMap": true,
"jsx": "react",
"moduleResolution": "node",
"moduleResolution": "bundler",
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
@ -18,12 +17,16 @@
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"baseUrl": "./src",
"paths": {
"~/*": ["./*"]
},
"importHelpers": true,
"skipLibCheck": true
"skipLibCheck": true,
"noErrorTruncation": true,
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"],
"exclude": ["node_modules", "src/__tests__/cypress"]
"exclude": ["node_modules", "dist", "public-cypress", "src/__tests__/cypress"]
}