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 # Test against prod build hosted by lightweight http server
BASE_URL=http://localhost:9001 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 stats.json
coverage coverage
.idea .idea
.env
.vscode/* .vscode/*
!.vscode/settings.json !.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 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: 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`.
```bash
npm run start:dev:mock
```
### Testing ### 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 workspacekind
workspacekinds workspacekinds
healthcheck 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, relativeDir,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css', '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,186 +1,245 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path'); const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin'); const CopyPlugin = require('copy-webpack-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); const { setupWebpackDotenvFilesForEnv } = require('./dotenv');
const Dotenv = require('dotenv-webpack'); const { name } = require('../package.json');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const { EnvironmentPlugin } = require('webpack'); const RELATIVE_DIRNAME = process.env._RELATIVE_DIRNAME;
const APP_PREFIX = process.env.APP_PREFIX || '/workspaces'; const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
const IMAGES_DIRNAME = 'images'; const IMAGES_DIRNAME = process.env._IMAGES_DIRNAME;
const relativeDir = path.resolve(__dirname, '..'); const PUBLIC_PATH = process.env._PUBLIC_PATH;
module.exports = (env) => { const SRC_DIR = process.env._SRC_DIR;
return { const COMMON_DIR = process.env._COMMON_DIR;
module: { const DIST_DIR = process.env._DIST_DIR;
rules: [ const OUTPUT_ONLY = process.env._OUTPUT_ONLY;
{ const FAVICON = process.env.FAVICON;
test: /\.(tsx|ts|jsx)?$/, const PRODUCT_NAME = process.env.PRODUCT_NAME;
use: [ const COVERAGE = process.env.COVERAGE;
{ const DEPLOYMENT_MODE = process.env._DEPLOYMENT_MODE;
loader: 'ts-loader', const BASE_PATH = DEPLOYMENT_MODE === 'kubeflow' ? '/workspaces' : PUBLIC_PATH;
options: {
experimentalWatchApi: true, 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|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: {
transpileOnly: true,
},
}, },
}, ],
], },
{
test: /\.(svg|ttf|eot|woff|woff2)$/,
// only process modules with this loader
// if they live under a 'fonts' or 'pficon' directory
include: [
path.resolve(RELATIVE_DIRNAME, 'node_modules/patternfly/dist/fonts'),
path.resolve(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/dist/styles/assets/fonts',
),
path.resolve(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/dist/styles/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',
options: {
// Limit at 50k. larger files emitted into separate files
limit: 5000,
outputPath: 'fonts',
name: '[name].[ext]',
},
}, },
{ },
test: /\.(svg|ttf|eot|woff|woff2)$/, {
type: 'asset/resource', test: /\.svg$/,
// only process modules with this loader include: (input) => input.indexOf('background-filter.svg') > 1,
// if they live under a 'fonts' or 'pficon' directory use: [
include: [ {
path.resolve(relativeDir, 'node_modules/patternfly/dist/fonts'), loader: 'url-loader',
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: { options: {
// Limit at 50k. larger files emitted into separate files
limit: 5000, limit: 5000,
outputPath: 'fonts', 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(IMAGES_DIRNAME) > -1,
use: {
loader: 'svg-url-loader',
options: {
limit: 10000,
},
}, },
{ },
test: /\.svg$/, {
include: (input) => input.indexOf('background-filter.svg') > 1, test: /\.svg$/,
use: [ // 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
loader: 'url-loader', include: (input) =>
options: { input.indexOf(IMAGES_DIRNAME) === -1 &&
limit: 5000, input.indexOf('fonts') === -1 &&
outputPath: 'svgs', input.indexOf('background-filter') === -1 &&
name: '[name].[ext]', input.indexOf('pficon') === -1,
}, use: {
}, loader: 'raw-loader',
], options: {},
}, },
{ },
test: /\.svg$/, {
// only process SVG modules with this loader if they live under a 'bgimages' directory test: /\.(jpg|jpeg|png|gif)$/i,
// this is primarily useful when applying a CSS background using an SVG include: [
include: (input) => input.indexOf(IMAGES_DIRNAME) > -1, SRC_DIR,
use: { COMMON_DIR,
loader: 'svg-url-loader', 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(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/dist/styles/assets/images',
),
path.resolve(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
RELATIVE_DIRNAME,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images',
),
],
use: [
{
loader: 'url-loader',
options: { options: {
limit: 10000, limit: 5000,
outputPath: 'images',
name: '[name].[ext]',
}, },
}, },
}, ],
{ },
test: /\.svg$/, {
// only process SVG modules with this loader when they don't live under a 'bgimages', test: /\.s[ac]ss$/i,
// 'fonts', or 'pficon' directory, those are handled with other loaders use: [
include: (input) => // Creates `style` nodes from JS strings
input.indexOf(IMAGES_DIRNAME) === -1 && 'style-loader',
input.indexOf('fonts') === -1 && // Translates CSS into CommonJS
input.indexOf('background-filter') === -1 && 'css-loader',
input.indexOf('pficon') === -1, // Compiles Sass to CSS
use: { 'sass-loader',
loader: 'raw-loader', ],
options: {}, },
}, {
}, test: /\.ya?ml$/,
{ use: 'js-yaml-loader',
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'),
path.resolve(
relativeDir,
'node_modules/@patternfly/react-core/dist/styles/assets/images',
),
path.resolve(
relativeDir,
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
relativeDir,
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images',
),
path.resolve(
relativeDir,
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images',
),
],
type: 'asset/inline',
use: [
{
options: {
limit: 5000,
outputPath: 'images',
name: '[name].[ext]',
},
},
],
},
{
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',
],
},
{
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',
),
],
},
],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(relativeDir, 'dist'),
publicPath: APP_PREFIX,
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(relativeDir, 'src', 'index.html'),
}),
new Dotenv({
systemvars: true,
silent: true,
}),
new CopyPlugin({
patterns: [{ from: './src/images', to: 'images' }],
}),
new ForkTsCheckerWebpackPlugin(),
new EnvironmentPlugin({
APP_PREFIX: process.env.APP_PREFIX || '/workspaces',
}),
], ],
resolve: { },
extensions: ['.js', '.ts', '.tsx', '.jsx'], output: {
plugins: [ filename: '[name].bundle.js',
new TsconfigPathsPlugin({ path: DIST_DIR,
configFile: path.resolve(relativeDir, './tsconfig.json'), publicPath: BASE_PATH,
}), uniqueName: name,
},
plugins: [
...setupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
isRoot: IS_PROJECT_ROOT_DIR,
}),
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: 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,
},
], ],
symlinks: false, }),
cacheWithContext: false, ],
resolve: {
extensions: ['.js', '.ts', '.tsx', '.jsx'],
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 path = require('path');
const { EnvironmentPlugin } = require('webpack');
const { merge } = require('webpack-merge'); const { merge } = require('webpack-merge');
const common = require('./webpack.common.js'); const { setupWebpackDotenvFilesForEnv, setupDotenvFilesForEnv } = require('./dotenv');
const { stylePaths } = require('./stylePaths'); const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const HOST = process.env.HOST || 'localhost'; const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const PORT = process.env.PORT || '9000'; const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
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';
module.exports = merge(common('development'), { const smp = new SpeedMeasurePlugin({ disable: !process.env.MEASURE });
mode: 'development',
devtool: 'eval-source-map', setupDotenvFilesForEnv({ env: 'development' });
devServer: { const webpackCommon = require('./webpack.common.js');
host: HOST,
port: PORT, const RELATIVE_DIRNAME = process.env._RELATIVE_DIRNAME;
historyApiFallback: { const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
index: APP_PREFIX + '/index.html', 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,
}),
],
}, },
open: [APP_PREFIX], webpackCommon('development'),
static: { {
directory: path.resolve(relativeDir, 'dist'), mode: 'development',
publicPath: APP_PREFIX, devtool: 'eval-source-map',
}, optimization: {
client: { runtimeChunk: 'single',
overlay: true, removeEmptyChunks: true,
}, },
proxy: [ devServer: {
{ host: HOST,
context: ['/api'], port: PORT,
target: { compress: true,
host: PROXY_HOST, historyApiFallback: {
protocol: PROXY_PROTOCOL, index: `${BASE_PATH}/index.html`,
port: PROXY_PORT, },
hot: true,
open: [BASE_PATH],
proxy: [
{
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`,
);
}
}, },
changeOrigin: true,
}, },
], module: {
}, rules: [
module: { {
rules: [ test: /\.css$/,
{ include: [
test: /\.css$/, SRC_DIR,
include: [...stylePaths], COMMON_DIR,
use: ['style-loader', 'css-loader'], path.resolve(RELATIVE_DIRNAME, 'node_modules/@patternfly'),
],
use: ['style-loader', 'css-loader'],
},
],
}, },
], plugins: [
}, new ForkTsCheckerWebpackPlugin(),
plugins: [ new ReactRefreshWebpackPlugin({ overlay: false }),
new EnvironmentPlugin({ ],
WEBPACK_REPLACE__mockApiEnabled: MOCK_API_ENABLED, },
}), ),
], );
});

View File

@ -1,44 +1,61 @@
/* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path');
const { merge } = require('webpack-merge'); 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 MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserJSPlugin = require('terser-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;
mode: 'production', const IS_PROJECT_ROOT_DIR = process.env._IS_PROJECT_ROOT_DIR;
devtool: 'source-map', const SRC_DIR = process.env._SRC_DIR;
optimization: { const COMMON_DIR = process.env._COMMON_DIR;
minimizer: [ const DIST_DIR = process.env._DIST_DIR;
new TerserJSPlugin({}), const OUTPUT_ONLY = process.env._OUTPUT_ONLY;
new CssMinimizerPlugin({
minimizerOptions: { if (OUTPUT_ONLY !== 'true') {
preset: ['default', { mergeLonghand: false }], console.info(`Cleaning OUTPUT DIR...\n ${DIST_DIR}\n`);
}, }
module.exports = merge(
{
plugins: [
...setupWebpackDotenvFilesForEnv({
directory: RELATIVE_DIRNAME,
env: 'production',
isRoot: IS_PROJECT_ROOT_DIR,
}), }),
], ],
}, },
plugins: [ webpackCommon('production'),
new MiniCssExtractPlugin({ {
filename: '[name].css', mode: 'production',
chunkFilename: '[name].bundle.css', devtool: 'source-map',
}), optimization: {
new EnvironmentPlugin({ minimize: true,
PRODUCTION, minimizer: [new TerserJSPlugin(), new CssMinimizerPlugin()],
}), },
], plugins: [
module: { new MiniCssExtractPlugin({
rules: [ filename: '[name].css',
{ chunkFilename: '[name].bundle.css',
test: /\.css$/, ignoreOrder: true,
include: [...stylePaths], }),
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
], ],
module: {
rules: [
{
test: /\.css$/,
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-profile": "webpack --config ./config/webpack.prod.js --profile --json > ./bundle.stats.json",
"build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json", "build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json",
"build:clean": "rimraf ./dist", "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", "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": "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:type-check test:unit test:cypress-ci",
"test": "run-s prettier:check test:lint 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: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 -- {@}\" -- ",
"test:jest": "jest --passWithNoTests", "test:jest": "jest --passWithNoTests",
"test:unit": "npm run test:jest -- --silent", "test:unit": "npm run test:jest -- --silent",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
"test:lint": "eslint --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src", "test:lint": "eslint --max-warnings 0 --ext .js,.ts,.jsx,.tsx ./src",
"test:lint:fix": "eslint --ext .js,.ts,.jsx,.tsx ./src --fix", "test:lint:fix": "eslint --ext .js,.ts,.jsx,.tsx ./src --fix",
"test:type-check": "tsc --noEmit",
"test:fix": "run-s prettier test:lint:fix", "test:fix": "run-s prettier test:lint:fix",
"cypress:open": "cypress open --project src/__tests__/cypress", "cypress:open": "cypress open --project src/__tests__/cypress",
"cypress:open:mock": "CY_MOCK=1 CY_WS_PORT=9002 npm run cypress:open -- ", "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": "cypress run -b chrome --project src/__tests__/cypress",
"cypress:run:mock": "CY_MOCK=1 npm run cypress:run -- ", "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", "cypress:server": "serve ./dist -p 9001 -s -L",
"prettier": "prettier --ignore-path .gitignore --write \"**/*{.ts,.tsx,.js,.cjs,.jsx,.css,.json}\"", "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}\"", "prettier:check": "prettier --ignore-path .gitignore --check \"**/*{.ts,.tsx,.js,.cjs,.jsx,.css,.json}\"",
"prepare": "cd ../../ && husky workspaces/frontend/.husky" "prepare": "cd ../../ && husky workspaces/frontend/.husky"
}, },
"devDependencies": { "devDependencies": {
"@cspell/eslint-plugin": "^9.1.2", "@mui/icons-material": "^6.4.8",
"@cypress/code-coverage": "^3.13.5",
"@mui/icons-material": "^6.3.1",
"@mui/material": "^6.3.1", "@mui/material": "^6.3.1",
"@mui/types": "^7.2.21", "@mui/types": "^7.2.21",
"@testing-library/cypress": "^10.0.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@testing-library/dom": "^10.4.0", "@swc/core": "^1.9.1",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "14.4.3",
"@types/chai-subset": "^1.3.5", "@types/chai-subset": "^1.3.5",
"@types/jest": "^29.5.3", "@types/classnames": "^2.3.1",
"@types/react-router-dom": "^5.3.3", "@types/dompurify": "^3.2.0",
"@types/victory": "^33.1.5", "@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", "chai-subset": "^1.6.0",
"concurrently": "^9.1.0", "concurrently": "^9.1.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^13.0.0",
"core-js": "^3.39.0", "core-js": "^3.40.0",
"cross-env": "^7.0.3", "css-loader": "^7.1.2",
"css-loader": "^6.11.0", "css-minimizer-webpack-plugin": "^7.0.0",
"css-minimizer-webpack-plugin": "^5.0.1", "dotenv": "^16.5.0",
"cypress": "^13.16.1", "dotenv-expand": "^5.1.0",
"cypress-axe": "^1.5.0", "dotenv-webpack": "^6.0.0",
"cypress-high-resolution": "^1.0.0", "expect": "^30.0.2",
"cypress-mochawesome-reporter": "^3.8.2", "file-loader": "^6.2.0",
"cypress-multi-reporters": "^2.0.4", "fork-ts-checker-webpack-plugin": "^9.0.2",
"dotenv": "^16.4.5", "html-webpack-plugin": "^5.6.3",
"dotenv-webpack": "^8.1.0",
"expect": "^29.7.0",
"fork-ts-checker-webpack-plugin": "^9.0.3",
"html-webpack-plugin": "^5.6.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"imagemin": "^8.0.1", "imagemin": "^9.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-environment-jsdom": "^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", "mini-css-extract-plugin": "^2.9.0",
"postcss": "^8.4.38", "postcss": "^8.4.49",
"prettier": "^3.3.0", "prettier": "^3.3.3",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react-router-dom": "^6.26.1", "react-refresh": "^0.14.2",
"regenerator-runtime": "^0.13.11", "regenerator-runtime": "^0.14.1",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"sass": "^1.83.4", "sass": "^1.87.0",
"sass-loader": "^16.0.4", "sass-loader": "^16.0.0",
"serve": "^14.2.1", "speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^3.3.4", "style-loader": "^4.0.0",
"svg-url-loader": "^8.0.0", "svg-url-loader": "^8.0.0",
"terser-webpack-plugin": "^5.3.10", "swagger-typescript-api": "13.2.7",
"ts-jest": "^29.1.4", "swc-loader": "^0.2.6",
"ts-loader": "^9.5.1", "terser-webpack-plugin": "^5.3.11",
"ts-loader": "^9.5.2",
"tsconfig-paths-webpack-plugin": "^4.1.0", "tsconfig-paths-webpack-plugin": "^4.1.0",
"tslib": "^2.6.0", "tslib": "^2.7.0",
"typescript": "^5.4.5", "typescript": "^5.8.2",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.91.0", "webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.2", "webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^5.1.4", "webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2", "webpack-dev-server": "^5.2.0",
"webpack-merge": "^5.10.0" "webpack-merge": "^6.0.1"
}, },
"dependencies": { "dependencies": {
"@patternfly/patternfly": "^6.3.1", "@patternfly/patternfly": "^6.3.1",
@ -112,35 +109,58 @@
"@patternfly/react-icons": "^6.3.1", "@patternfly/react-icons": "^6.3.1",
"@patternfly/react-styles": "^6.3.1", "@patternfly/react-styles": "^6.3.1",
"@patternfly/react-table": "^6.3.1", "@patternfly/react-table": "^6.3.1",
"@patternfly/react-templates": "^6.3.1",
"@patternfly/react-tokens": "^6.3.1", "@patternfly/react-tokens": "^6.3.1",
"@types/js-yaml": "^4.0.9", "@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/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@typescript-eslint/eslint-plugin": "^8.8.1", "axios": "^1.10.0",
"@typescript-eslint/parser": "^8.12.2", "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": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-node": "^0.3.7", "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-cypress": "^3.3.0",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.10.2", "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-only-tests": "^3.1.0",
"eslint-plugin-no-relative-import-paths": "^1.5.2", "eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.4.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.2.0",
"swagger-typescript-api": "^13.2.7" "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'; import coverage from '@cypress/code-coverage/task';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available // @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'; import cypressHighResolution from 'cypress-high-resolution';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no types available // @ts-ignore no types available
import { beforeRunHook, afterRunHook } from 'cypress-mochawesome-reporter/lib'; import { beforeRunHook, afterRunHook } from 'cypress-mochawesome-reporter/lib';
import { mergeFiles } from 'junit-report-merger'; import { mergeFiles } from 'junit-report-merger';
import { env, BASE_URL } from '~/__tests__/cypress/cypress/utils/testConfig'; 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'}`; const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`;
@ -41,7 +45,6 @@ export default defineConfig({
env: { env: {
MOCK: !!env.CY_MOCK, MOCK: !!env.CY_MOCK,
coverage: !!env.CY_COVERAGE, coverage: !!env.CY_COVERAGE,
APP_PREFIX: env.APP_PREFIX || '/workspaces',
codeCoverage: { codeCoverage: {
exclude: [path.resolve(__dirname, '../../third_party/**')], exclude: [path.resolve(__dirname, '../../third_party/**')],
}, },
@ -49,12 +52,20 @@ export default defineConfig({
}, },
defaultCommandTimeout: 10000, defaultCommandTimeout: 10000,
e2e: { 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`, specPattern: env.CY_MOCK ? `cypress/tests/mocked/**/*.cy.ts` : `cypress/tests/e2e/**/*.cy.ts`,
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,
setupNodeEvents(on, config) { setupNodeEvents(on, config) {
cypressHighResolution(on, config); cypressHighResolution(on, config);
coverage(on, config); coverage(on, config);
// Configure webpack preprocessor with custom webpack config
const options = {
webpackOptions: webpackConfig,
watchOptions: {},
};
on('file:preprocessor', webpack(options));
on('task', { on('task', {
readJSON(filePath: string) { readJSON(filePath: string) {
const absPath = path.resolve(__dirname, filePath); const absPath = path.resolve(__dirname, filePath);

View File

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

View File

@ -6,7 +6,7 @@ describe('WorkspaceDetailsActivity Component', () => {
cy.intercept('GET', 'api/v1/workspaces', { cy.intercept('GET', 'api/v1/workspaces', {
body: mockBFFResponse(mockWorkspaces), body: mockBFFResponse(mockWorkspaces),
}).as('getWorkspaces'); }).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 // 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 { JestAssertionError } from 'expect';
import 'core-js/actual/array/to-sorted'; import 'core-js/actual/array/to-sorted';
import { import {
@ -7,7 +7,10 @@ import {
createComparativeValue, createComparativeValue,
} from '~/__tests__/unit/testUtils/hooks'; } 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) => { const tryExpect = (expectFn: () => void) => {
try { 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 ErrorBoundary from '~/app/error/ErrorBoundary';
import NamespaceSelector from '~/shared/components/NamespaceSelector'; import NamespaceSelector from '~/shared/components/NamespaceSelector';
import logoDarkTheme from '~/images/logo-dark-theme.svg'; 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 { NamespaceContextProvider } from './context/NamespaceContextProvider';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
import NavSidebar from './NavSidebar'; import NavSidebar from './NavSidebar';
import { NotebookContextProvider } from './context/NotebookContext'; import { NotebookContextProvider } from './context/NotebookContext';
import { isMUITheme, Theme } from './const';
import { BrowserStorageContextProvider } from './context/BrowserStorageContext'; import { BrowserStorageContextProvider } from './context/BrowserStorageContext';
const isStandalone = process.env.PRODUCTION !== 'true'; const isStandalone = DEPLOYMENT_MODE === DeploymentMode.Standalone;
const App: React.FC = () => { const App: React.FC = () => {
useEffect(() => { useEffect(() => {

View File

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

View File

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

View File

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

View File

@ -504,12 +504,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
})} })}
{canCreateWorkspaces && ( {canCreateWorkspaces && (
<ToolbarItem> <ToolbarItem>
<Button <Button variant="primary" ouiaId="Primary" onClick={createWorkspace}>
size="lg"
variant="primary"
ouiaId="Primary"
onClick={createWorkspace}
>
Create workspace Create workspace
</Button> </Button>
</ToolbarItem> </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 React, { ReactNode, useMemo } from 'react';
import { APP_PREFIX, BFF_API_VERSION } from '~/app/const';
import EnsureAPIAvailability from '~/app/EnsureAPIAvailability'; import EnsureAPIAvailability from '~/app/EnsureAPIAvailability';
import { BFF_API_PREFIX, BFF_API_VERSION } from '~/shared/utilities/const';
import useNotebookAPIState, { NotebookAPIState } from './useNotebookAPIState'; import useNotebookAPIState, { NotebookAPIState } from './useNotebookAPIState';
export type NotebookContextType = { export type NotebookContextType = {
@ -19,8 +19,8 @@ interface NotebookContextProviderProps {
} }
export const NotebookContextProvider: React.FC<NotebookContextProviderProps> = ({ children }) => { export const NotebookContextProvider: React.FC<NotebookContextProviderProps> = ({ children }) => {
// Remove trailing slash from APP_PREFIX to avoid double slashes // Remove trailing slash from BFF_API_PREFIX to avoid double slashes
const cleanPrefix = APP_PREFIX.replace(/\/$/, ''); const cleanPrefix = BFF_API_PREFIX.replace(/\/$/, '');
const hostPath = `${cleanPrefix}/api/${BFF_API_VERSION}`; const hostPath = `${cleanPrefix}/api/${BFF_API_VERSION}`;
const [apiState, refreshAPIState] = useNotebookAPIState(hostPath); 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 { APIState } from '~/shared/api/types';
import useAPIState from '~/shared/api/useAPIState'; import useAPIState from '~/shared/api/useAPIState';
import { mockNotebookApisImpl } from '~/shared/mock/mockNotebookApis'; import { mockNotebookApisImpl } from '~/shared/mock/mockNotebookApis';
import { MOCK_API_ENABLED } from '~/shared/utilities/const';
export type NotebookAPIState = APIState<NotebookApis>; export type NotebookAPIState = APIState<NotebookApis>;
const MOCK_API_ENABLED = process.env.WEBPACK_REPLACE__mockApiEnabled === 'true';
const useNotebookAPIState = ( const useNotebookAPIState = (
hostPath: string | null, hostPath: string | null,
): [apiState: NotebookAPIState, refreshAPIState: () => void] => { ): [apiState: NotebookAPIState, refreshAPIState: () => void] => {

View File

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

View File

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

View File

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

View File

@ -8,11 +8,11 @@ import { useTypedLocation, useTypedNavigate, useTypedParams } from '~/app/router
import WorkspaceTable, { WorkspaceTableRef } from '~/app/components/WorkspaceTable'; import WorkspaceTable, { WorkspaceTableRef } from '~/app/components/WorkspaceTable';
import { useWorkspacesByKind } from '~/app/hooks/useWorkspaces'; import { useWorkspacesByKind } from '~/app/hooks/useWorkspaces';
import WorkspaceKindSummaryExpandableCard from '~/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard'; import WorkspaceKindSummaryExpandableCard from '~/app/pages/WorkspaceKinds/summary/WorkspaceKindSummaryExpandableCard';
import { DEFAULT_POLLING_RATE_MS } from '~/app/const';
import { LoadingSpinner } from '~/app/components/LoadingSpinner'; import { LoadingSpinner } from '~/app/components/LoadingSpinner';
import { LoadError } from '~/app/components/LoadError'; import { LoadError } from '~/app/components/LoadError';
import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions'; import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions';
import { usePolling } from '~/app/hooks/usePolling'; import { usePolling } from '~/app/hooks/usePolling';
import { POLL_INTERVAL } from '~/shared/utilities/const';
const WorkspaceKindSummary: React.FC = () => { const WorkspaceKindSummary: React.FC = () => {
const [isSummaryExpanded, setIsSummaryExpanded] = useState(true); const [isSummaryExpanded, setIsSummaryExpanded] = useState(true);
@ -30,7 +30,7 @@ const WorkspaceKindSummary: React.FC = () => {
podConfigId, podConfigId,
}); });
usePolling(refreshWorkspaces, DEFAULT_POLLING_RATE_MS); usePolling(refreshWorkspaces, POLL_INTERVAL);
const tableRowActions = useWorkspaceRowActions([{ id: 'viewDetails' }]); 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 WorkspaceTable from '~/app/components/WorkspaceTable';
import { useNamespaceContext } from '~/app/context/NamespaceContextProvider'; import { useNamespaceContext } from '~/app/context/NamespaceContextProvider';
import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces'; import { useWorkspacesByNamespace } from '~/app/hooks/useWorkspaces';
import { DEFAULT_POLLING_RATE_MS } from '~/app/const';
import { LoadingSpinner } from '~/app/components/LoadingSpinner'; import { LoadingSpinner } from '~/app/components/LoadingSpinner';
import { LoadError } from '~/app/components/LoadError'; import { LoadError } from '~/app/components/LoadError';
import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions'; import { useWorkspaceRowActions } from '~/app/hooks/useWorkspaceRowActions';
import { usePolling } from '~/app/hooks/usePolling'; import { usePolling } from '~/app/hooks/usePolling';
import { WorkspacesWorkspaceState } from '~/generated/data-contracts'; import { WorkspacesWorkspaceState } from '~/generated/data-contracts';
import { POLL_INTERVAL } from '~/shared/utilities/const';
export const Workspaces: React.FunctionComponent = () => { export const Workspaces: React.FunctionComponent = () => {
const { selectedNamespace } = useNamespaceContext(); const { selectedNamespace } = useNamespaceContext();
@ -18,7 +18,7 @@ export const Workspaces: React.FunctionComponent = () => {
const [workspaces, workspacesLoaded, workspacesLoadError, refreshWorkspaces] = const [workspaces, workspacesLoaded, workspacesLoadError, refreshWorkspaces] =
useWorkspacesByNamespace(selectedNamespace); useWorkspacesByNamespace(selectedNamespace);
usePolling(refreshWorkspaces, DEFAULT_POLLING_RATE_MS); usePolling(refreshWorkspaces, POLL_INTERVAL);
const tableRowActions = useWorkspaceRowActions([ const tableRowActions = useWorkspaceRowActions([
{ id: 'viewDetails' }, { id: 'viewDetails' },

View File

@ -62,7 +62,7 @@ type NavigateOptions<T extends AppRouteKey> = CommonNavigateOptions &
* Go to my route * Go to my route
* </Link> * </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]); 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 ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles'; import { ThemeProvider, createTheme } from '@mui/material/styles';
import { URL_PREFIX } from '~/shared/utilities/const';
import App from './app/App'; import App from './app/App';
import { APP_PREFIX } from './app/const';
const theme = createTheme({ cssVariables: true }); const theme = createTheme({ cssVariables: true });
const root = ReactDOM.createRoot(document.getElementById('root')!); const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Router basename={APP_PREFIX}> <Router basename={URL_PREFIX}>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<App /> <App />
</ThemeProvider> </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 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 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 const CONTENT_TYPE_KEY = 'Content-Type';
export enum ContentType { export const isMUITheme = (): boolean => STYLE_THEME === Theme.MUI;
YAML = 'application/yaml',
}

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