feat(ws): Notebooks 2.0 // Frontend // Workspaces table // Live mockup (#140)
* Workspaces initial frontend Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> * Fixing a CVE and cypress test Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com> --------- Signed-off-by: paulovmr <832830+paulovmr@users.noreply.github.com> Signed-off-by: Eder Ignatowicz <ignatowicz@gmail.com> Co-authored-by: Eder Ignatowicz <ignatowicz@gmail.com>
This commit is contained in:
parent
e920dd99de
commit
68d0b91c2f
|
@ -0,0 +1 @@
|
|||
package.json
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
|
@ -7,9 +7,21 @@ module.exports = {
|
|||
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-styles/css'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/base.css'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css')
|
||||
]
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly',
|
||||
),
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css',
|
||||
),
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css',
|
||||
),
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css',
|
||||
),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -19,10 +19,10 @@ module.exports = (env) => {
|
|||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
experimentalWatchApi: true
|
||||
}
|
||||
}
|
||||
]
|
||||
experimentalWatchApi: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(svg|ttf|eot|woff|woff2)$/,
|
||||
|
@ -31,10 +31,16 @@ module.exports = (env) => {
|
|||
// if they live under a 'fonts' or 'pficon' directory
|
||||
include: [
|
||||
path.resolve(relativeDir, 'node_modules/patternfly/dist/fonts'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'),
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'),
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/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')
|
||||
path.resolve(relativeDir, 'node_modules/@patternfly/patternfly/assets/pficon'),
|
||||
],
|
||||
use: {
|
||||
loader: 'file-loader',
|
||||
|
@ -93,19 +99,22 @@ module.exports = (env) => {
|
|||
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'
|
||||
'node_modules/@patternfly/react-core/dist/styles/assets/images',
|
||||
),
|
||||
path.resolve(
|
||||
relativeDir,
|
||||
'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images'
|
||||
'node_modules/@patternfly/react-core/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'
|
||||
)
|
||||
'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: [
|
||||
|
@ -113,10 +122,10 @@ module.exports = (env) => {
|
|||
options: {
|
||||
limit: 5000,
|
||||
outputPath: 'images',
|
||||
name: '[name].[ext]'
|
||||
}
|
||||
}
|
||||
]
|
||||
name: '[name].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.s[ac]ss$/i,
|
||||
|
@ -128,35 +137,35 @@ module.exports = (env) => {
|
|||
// Compiles Sass to CSS
|
||||
'sass-loader',
|
||||
],
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
filename: '[name].bundle.js',
|
||||
path: path.resolve(relativeDir, 'dist'),
|
||||
publicPath: ASSET_PATH
|
||||
publicPath: ASSET_PATH,
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(relativeDir, 'src', 'index.html')
|
||||
template: path.resolve(relativeDir, 'src', 'index.html'),
|
||||
}),
|
||||
new Dotenv({
|
||||
systemvars: true,
|
||||
silent: true
|
||||
silent: true,
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [{ from: './src/images', to: 'images' }]
|
||||
})
|
||||
patterns: [{ from: './src/images', to: 'images' }],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.js', '.ts', '.tsx', '.jsx'],
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: path.resolve(relativeDir, './tsconfig.json')
|
||||
})
|
||||
configFile: path.resolve(relativeDir, './tsconfig.json'),
|
||||
}),
|
||||
],
|
||||
symlinks: false,
|
||||
cacheWithContext: false
|
||||
}
|
||||
cacheWithContext: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = merge(common('development'), {
|
|||
},
|
||||
proxy: [
|
||||
{
|
||||
context: ["/api"],
|
||||
context: ['/api'],
|
||||
target: {
|
||||
host: PROXY_HOST,
|
||||
protocol: PROXY_PROTOCOL,
|
||||
|
|
|
@ -15,24 +15,24 @@ module.exports = merge(common('production'), {
|
|||
new TerserJSPlugin({}),
|
||||
new CssMinimizerPlugin({
|
||||
minimizerOptions: {
|
||||
preset: ['default', { mergeLonghand: false }]
|
||||
}
|
||||
})
|
||||
]
|
||||
preset: ['default', { mergeLonghand: false }],
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[name].bundle.css'
|
||||
})
|
||||
chunkFilename: '[name].bundle.css',
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: [...stylePaths],
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader']
|
||||
}
|
||||
]
|
||||
}
|
||||
use: [MiniCssExtractPlugin.loader, 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
@ -12,21 +12,19 @@ module.exports = {
|
|||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
moduleDirectories: [
|
||||
'node_modules',
|
||||
'<rootDir>/src'
|
||||
],
|
||||
moduleDirectories: ['node_modules', '<rootDir>/src'],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less)$': '<rootDir>/__mocks__/styleMock.js',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
|
||||
'@app/(.*)': '<rootDir>/src/app/$1'
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
|
||||
'<rootDir>/__mocks__/fileMock.js',
|
||||
'@app/(.*)': '<rootDir>/src/app/$1',
|
||||
},
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
|
||||
// The test environment that will be used for testing.
|
||||
testEnvironment: 'jsdom'
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
||||
|
|
|
@ -9,9 +9,10 @@
|
|||
"version": "0.0.1",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@patternfly/react-core": "6.0.0-alpha.68",
|
||||
"@patternfly/react-icons": "6.0.0-alpha.24",
|
||||
"@patternfly/react-styles": "6.0.0-alpha.24",
|
||||
"@patternfly/react-core": "^6.0.0",
|
||||
"@patternfly/react-icons": "^6.0.0",
|
||||
"@patternfly/react-styles": "^6.0.0",
|
||||
"@patternfly/react-table": "^6.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
@ -3298,16 +3299,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-core": {
|
||||
"version": "6.0.0-alpha.68",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0-alpha.68.tgz",
|
||||
"integrity": "sha512-YhZY4xuDF0WJyDAAzHdvhgYCECs5bY4+QYUbkPe+dGLNLnwVMU3ZwZlLD9ARHRwfXDRV3g+ttS8HRbigbHgtpQ==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0.tgz",
|
||||
"integrity": "sha512-UKFj9+YzBY+FfEDsLONgOM4N0e8SPV/27/UzNRiJ0gpgqbw2POuXwLpjGSRTTIUuCaLaGGM5PeTSj7mMB73ykw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@patternfly/react-icons": "^6.0.0-alpha.24",
|
||||
"@patternfly/react-styles": "^6.0.0-alpha.24",
|
||||
"@patternfly/react-tokens": "^6.0.0-alpha.24",
|
||||
"focus-trap": "7.5.4",
|
||||
"@patternfly/react-icons": "^6.0.0",
|
||||
"@patternfly/react-styles": "^6.0.0",
|
||||
"@patternfly/react-tokens": "^6.0.0",
|
||||
"focus-trap": "7.6.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"tslib": "^2.6.2"
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
|
@ -3315,23 +3317,44 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-icons": {
|
||||
"version": "6.0.0-alpha.24",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0-alpha.24.tgz",
|
||||
"integrity": "sha512-SRW9sTaHTbMJu+lf7+vWe5fNI1hfquZB6xlJe54D4WAzgzPDTxwPRi6I/KEboKBMZOvU/FYxH2/L5TCYmH51Hw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.0.0.tgz",
|
||||
"integrity": "sha512-ZFrsBVKrAp0DZrPOss98OA/EVUL4F0frXhR1uBId9+3ZrRArdKTgYgmQUCeSzMbxnSlxpmm3a2L05XQ36VUVbw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
"react-dom": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-styles": {
|
||||
"version": "6.0.0-alpha.24",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.0.0-alpha.24.tgz",
|
||||
"integrity": "sha512-9Gh0CbeShaNOJSRy/Y7dEx+Nc9rlDrgjSF9rMtjlFTLfU7HvVavrfuBTGV5NpFJlVBtudJAsDaNRTlC22kKokg=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.0.0.tgz",
|
||||
"integrity": "sha512-fJFMB89sTRGlZTzTLmpRmthgOXqcN078scHMFJ3ttfi2D2btnem5oZrxmQ/gPZkZOxR+9MqwKDB6l3F5x1SqLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@patternfly/react-table": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.0.0.tgz",
|
||||
"integrity": "sha512-LvWMzjcQZHdFUpK8fjj5EAFrNxqB8/MFd7gUUZu7AgYt6rmS2im4xk6yb7h0K7cAhY085oPeRF9lkYSCgzlRDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@patternfly/react-core": "^6.0.0",
|
||||
"@patternfly/react-icons": "^6.0.0",
|
||||
"@patternfly/react-styles": "^6.0.0",
|
||||
"@patternfly/react-tokens": "^6.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
"react-dom": "^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/@patternfly/react-tokens": {
|
||||
"version": "6.0.0-alpha.24",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0-alpha.24.tgz",
|
||||
"integrity": "sha512-jIVaGxxZD8Wsp2Xbf8z9mrpfYQx4NzlWlUza0IoTuwslEdcxt77Yo3sh0qlyfRBDNx+Q01tdEFXftJ+6OZQ3Gw=="
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0.tgz",
|
||||
"integrity": "sha512-xd0ynDkiIW2rp8jz4TNvR4Dyaw9kSMkZdsuYcLlFXCVmvX//Mnl4rhBnid/2j2TaqK0NbkyTTPnPY/BU7SfLVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pkgr/core": {
|
||||
"version": "0.1.1",
|
||||
|
@ -6911,10 +6934,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
|
@ -9714,9 +9738,10 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/focus-trap": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz",
|
||||
"integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.0.tgz",
|
||||
"integrity": "sha512-1td0l3pMkWJLFipobUcGaf+5DTY4PLDDrcqoSaKP8ediO/CoWCCYk/fT/Y2A4e6TNB+Sh6clRJCjOPPnKoNHnQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tabbable": "^6.2.0"
|
||||
}
|
||||
|
@ -13653,8 +13678,7 @@
|
|||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
|
@ -15035,9 +15059,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/npm-run-all/node_modules/cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"version": "6.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
|
||||
"integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nice-try": "^1.0.4",
|
||||
|
@ -18672,7 +18696,8 @@
|
|||
"node_modules/tabbable": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tcomb": {
|
||||
"version": "3.2.29",
|
||||
|
@ -19251,9 +19276,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
"build:bundle-analyze": "webpack-bundle-analyzer ./bundle.stats.json",
|
||||
"build:clean": "rimraf ./dist",
|
||||
"build:prod": "webpack --config ./config/webpack.prod.js",
|
||||
"prettier": "prettier --ignore-path .gitignore --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"",
|
||||
"prettier:check": "prettier --ignore-path .gitignore --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"",
|
||||
"start:dev": "webpack serve --hot --color --config ./config/webpack.dev.js",
|
||||
"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",
|
||||
|
@ -85,9 +87,10 @@
|
|||
"webpack-merge": "^5.10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@patternfly/react-core": "6.0.0-alpha.68",
|
||||
"@patternfly/react-icons": "6.0.0-alpha.24",
|
||||
"@patternfly/react-styles": "6.0.0-alpha.24",
|
||||
"@patternfly/react-core": "^6.0.0",
|
||||
"@patternfly/react-icons": "^6.0.0",
|
||||
"@patternfly/react-styles": "^6.0.0",
|
||||
"@patternfly/react-table": "^6.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
|
|
@ -11,112 +11,109 @@ import { beforeRunHook, afterRunHook } from 'cypress-mochawesome-reporter/lib';
|
|||
import { mergeFiles } from 'junit-report-merger';
|
||||
import { env, BASE_URL } from '~/__tests__/cypress/cypress/utils/testConfig';
|
||||
|
||||
|
||||
const resultsDir = `${env.CY_RESULTS_DIR || 'results'}/${env.CY_MOCK ? 'mocked' : 'e2e'}`;
|
||||
|
||||
export default defineConfig({
|
||||
experimentalMemoryManagement: true,
|
||||
// Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406
|
||||
reporter: '../../../node_modules/cypress-multi-reporters',
|
||||
reporterOptions: {
|
||||
reporterEnabled: 'cypress-mochawesome-reporter, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: `${resultsDir}/junit/junit-[hash].xml`,
|
||||
},
|
||||
cypressMochawesomeReporterReporterOptions: {
|
||||
charts: true,
|
||||
embeddedScreenshots: false,
|
||||
ignoreVideos: false,
|
||||
inlineAssets: true,
|
||||
reportDir: resultsDir,
|
||||
videoOnFailOnly: true,
|
||||
},
|
||||
experimentalMemoryManagement: true,
|
||||
// Use relative path as a workaround to https://github.com/cypress-io/cypress/issues/6406
|
||||
reporter: '../../../node_modules/cypress-multi-reporters',
|
||||
reporterOptions: {
|
||||
reporterEnabled: 'cypress-mochawesome-reporter, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
mochaFile: `${resultsDir}/junit/junit-[hash].xml`,
|
||||
},
|
||||
chromeWebSecurity: false,
|
||||
viewportWidth: 1920,
|
||||
viewportHeight: 1080,
|
||||
numTestsKeptInMemory: 1,
|
||||
video: true,
|
||||
screenshotsFolder: `${resultsDir}/screenshots`,
|
||||
videosFolder: `${resultsDir}/videos`,
|
||||
env: {
|
||||
MOCK: !!env.CY_MOCK,
|
||||
coverage: !!env.CY_COVERAGE,
|
||||
codeCoverage: {
|
||||
exclude: [path.resolve(__dirname, '../../third_party/**')],
|
||||
},
|
||||
resolution: 'high',
|
||||
cypressMochawesomeReporterReporterOptions: {
|
||||
charts: true,
|
||||
embeddedScreenshots: false,
|
||||
ignoreVideos: false,
|
||||
inlineAssets: true,
|
||||
reportDir: resultsDir,
|
||||
videoOnFailOnly: true,
|
||||
},
|
||||
defaultCommandTimeout: 10000,
|
||||
e2e: {
|
||||
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);
|
||||
on('task', {
|
||||
readJSON(filePath: string) {
|
||||
const absPath = path.resolve(__dirname, filePath);
|
||||
if (fs.existsSync(absPath)) {
|
||||
try {
|
||||
return Promise.resolve(JSON.parse(fs.readFileSync(absPath, 'utf8')));
|
||||
} catch {
|
||||
// return default value
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({});
|
||||
},
|
||||
log(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
error(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
return null;
|
||||
},
|
||||
table(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.table(message);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Delete videos for specs without failing or retried tests
|
||||
on('after:spec', (_, results) => {
|
||||
if (results.video) {
|
||||
// Do we have failures for any retry attempts?
|
||||
const failures = results.tests.some((test) =>
|
||||
test.attempts.some((attempt) => attempt.state === 'failed'),
|
||||
);
|
||||
if (!failures) {
|
||||
// delete the video if the spec passed and no tests retried
|
||||
fs.unlinkSync(results.video);
|
||||
},
|
||||
chromeWebSecurity: false,
|
||||
viewportWidth: 1920,
|
||||
viewportHeight: 1080,
|
||||
numTestsKeptInMemory: 1,
|
||||
video: true,
|
||||
screenshotsFolder: `${resultsDir}/screenshots`,
|
||||
videosFolder: `${resultsDir}/videos`,
|
||||
env: {
|
||||
MOCK: !!env.CY_MOCK,
|
||||
coverage: !!env.CY_COVERAGE,
|
||||
codeCoverage: {
|
||||
exclude: [path.resolve(__dirname, '../../third_party/**')],
|
||||
},
|
||||
resolution: 'high',
|
||||
},
|
||||
defaultCommandTimeout: 10000,
|
||||
e2e: {
|
||||
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);
|
||||
on('task', {
|
||||
readJSON(filePath: string) {
|
||||
const absPath = path.resolve(__dirname, filePath);
|
||||
if (fs.existsSync(absPath)) {
|
||||
try {
|
||||
return Promise.resolve(JSON.parse(fs.readFileSync(absPath, 'utf8')));
|
||||
} catch {
|
||||
// return default value
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on('before:run', async (details) => {
|
||||
// cypress-mochawesome-reporter
|
||||
await beforeRunHook(details);
|
||||
});
|
||||
|
||||
on('after:run', async () => {
|
||||
// cypress-mochawesome-reporter
|
||||
await afterRunHook();
|
||||
|
||||
// merge junit reports into a single report
|
||||
const outputFile = path.join(__dirname, resultsDir, 'junit-report.xml');
|
||||
const inputFiles = [`./${resultsDir}/junit/*.xml`];
|
||||
await mergeFiles(outputFile, inputFiles);
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
|
||||
return Promise.resolve({});
|
||||
},
|
||||
log(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
error(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(message);
|
||||
return null;
|
||||
},
|
||||
table(message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.table(message);
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Delete videos for specs without failing or retried tests
|
||||
on('after:spec', (_, results) => {
|
||||
if (results.video) {
|
||||
// Do we have failures for any retry attempts?
|
||||
const failures = results.tests.some((test) =>
|
||||
test.attempts.some((attempt) => attempt.state === 'failed'),
|
||||
);
|
||||
if (!failures) {
|
||||
// delete the video if the spec passed and no tests retried
|
||||
fs.unlinkSync(results.video);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on('before:run', async (details) => {
|
||||
// cypress-mochawesome-reporter
|
||||
await beforeRunHook(details);
|
||||
});
|
||||
|
||||
on('after:run', async () => {
|
||||
// cypress-mochawesome-reporter
|
||||
await afterRunHook();
|
||||
|
||||
// merge junit reports into a single report
|
||||
const outputFile = path.join(__dirname, resultsDir, 'junit-report.xml');
|
||||
const inputFiles = [`./${resultsDir}/junit/*.xml`];
|
||||
await mergeFiles(outputFile, inputFiles);
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
class Home {
|
||||
visit() {
|
||||
cy.visit(`/`);
|
||||
}
|
||||
|
||||
findButton() {
|
||||
return cy.get('button:contains("Primary Action")');
|
||||
}
|
||||
visit() {
|
||||
cy.visit(`/`);
|
||||
}
|
||||
|
||||
findButton() {
|
||||
return cy.get('button:contains("Create Workspace")');
|
||||
}
|
||||
}
|
||||
|
||||
export const home = new Home();
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
class PageNotFound {
|
||||
visit() {
|
||||
cy.visit(`/force-not-found-page`, {'failOnStatusCode': false});
|
||||
this.wait();
|
||||
}
|
||||
|
||||
private wait() {
|
||||
this.findPage();
|
||||
cy.testA11y();
|
||||
}
|
||||
|
||||
findPage() {
|
||||
return cy.get('h1:contains("404 Page not found")');
|
||||
}
|
||||
visit() {
|
||||
cy.visit(`/force-not-found-page`, { failOnStatusCode: false });
|
||||
this.wait();
|
||||
}
|
||||
|
||||
private wait() {
|
||||
this.findPage();
|
||||
cy.testA11y();
|
||||
}
|
||||
|
||||
findPage() {
|
||||
return cy.get('h1:contains("404 Page not found")');
|
||||
}
|
||||
}
|
||||
|
||||
export const pageNotfound = new PageNotFound();
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import { pageNotfound } from "~/__tests__/cypress/cypress/pages/pageNotFound";
|
||||
import { home } from "~/__tests__/cypress/cypress/pages/home";
|
||||
import { pageNotfound } from '~/__tests__/cypress/cypress/pages/pageNotFound';
|
||||
import { home } from '~/__tests__/cypress/cypress/pages/home';
|
||||
|
||||
describe('Application', () => {
|
||||
it('Page not found should render', () => {
|
||||
pageNotfound.visit();
|
||||
});
|
||||
|
||||
it('Page not found should render', () => {
|
||||
pageNotfound.visit()
|
||||
});
|
||||
|
||||
it('Home page should have primary button', () => {
|
||||
home.visit()
|
||||
home.findButton();
|
||||
});
|
||||
|
||||
it('Home page should have primary button', () => {
|
||||
home.visit();
|
||||
home.findButton();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
MastheadToggle,
|
||||
Page,
|
||||
PageToggleButton,
|
||||
Title
|
||||
Title,
|
||||
} from '@patternfly/react-core';
|
||||
import NavSidebar from './NavSidebar';
|
||||
import { BarsIcon } from '@patternfly/react-icons';
|
||||
|
@ -35,7 +35,7 @@ const App: React.FC = () => {
|
|||
|
||||
return (
|
||||
<Page
|
||||
mainContainerId='primary-app-container'
|
||||
mainContainerId="primary-app-container"
|
||||
masthead={masthead}
|
||||
isManagedSidebar
|
||||
sidebar={<NavSidebar />}
|
||||
|
@ -45,4 +45,4 @@ const App: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default App;
|
||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||
import { Route, Routes } from 'react-router-dom';
|
||||
import { NotFound } from './pages/notFound/NotFound';
|
||||
import { Settings } from './pages/Settings/Settings';
|
||||
import { Dashboard } from './pages/Dashboard/Dashboard';
|
||||
import { Workspaces } from './pages/Workspaces/Workspaces';
|
||||
|
||||
export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup =>
|
||||
'children' in navItem;
|
||||
|
@ -52,14 +52,12 @@ const AppRoutes: React.FC = () => {
|
|||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/" element={<Workspaces />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
{
|
||||
// TODO: Remove the linter skip when we implement authentication
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
isAdmin && (
|
||||
<Route path="/notebookSettings/*" element={<Settings />} />
|
||||
)
|
||||
isAdmin && <Route path="/notebookSettings/*" element={<Settings />} />
|
||||
}
|
||||
</Routes>
|
||||
);
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateFooter,
|
||||
EmptyStateVariant,
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent
|
||||
} from '@patternfly/react-core';
|
||||
import { CubesIcon } from '@patternfly/react-icons';
|
||||
|
||||
const Dashboard: React.FunctionComponent = () => (
|
||||
<PageSection>
|
||||
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Dashboard Module)" icon={CubesIcon}>
|
||||
<EmptyStateBody>
|
||||
<TextContent>
|
||||
<Text component="p">
|
||||
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple enough to use but
|
||||
flexible enough to meet a variety of needs.
|
||||
</Text>
|
||||
</TextContent>
|
||||
</EmptyStateBody><EmptyStateFooter>
|
||||
<Button variant="primary">Primary Action</Button>
|
||||
|
||||
</EmptyStateFooter></EmptyState>
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
export { Dashboard };
|
|
@ -7,24 +7,23 @@ import {
|
|||
EmptyStateFooter,
|
||||
EmptyStateVariant,
|
||||
PageSection,
|
||||
Text,
|
||||
TextContent
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
const Settings: React.FunctionComponent = () => (
|
||||
<PageSection>
|
||||
<EmptyState variant={EmptyStateVariant.full} titleText="Empty State (Stub Settings Module)" icon={CubesIcon}>
|
||||
<EmptyState
|
||||
variant={EmptyStateVariant.full}
|
||||
titleText="Empty State (Stub Settings Module)"
|
||||
icon={CubesIcon}
|
||||
>
|
||||
<EmptyStateBody>
|
||||
<TextContent>
|
||||
<Text component="p">
|
||||
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple enough to use but
|
||||
flexible enough to meet a variety of needs.
|
||||
</Text>
|
||||
</TextContent>
|
||||
</EmptyStateBody><EmptyStateFooter>
|
||||
<Button variant="primary">Primary Action</Button>
|
||||
|
||||
</EmptyStateFooter></EmptyState>
|
||||
This represents an the empty state pattern in Patternfly 6. Hopefully it's simple
|
||||
enough to use but flexible enough to meet a variety of needs.
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<Button variant="primary">Primary Action</Button>
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
</PageSection>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,479 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
PageSection,
|
||||
MenuToggle,
|
||||
TimestampTooltipVariant,
|
||||
Timestamp,
|
||||
Label,
|
||||
Title,
|
||||
Popper,
|
||||
MenuToggleElement,
|
||||
Menu,
|
||||
MenuContent,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarToggleGroup,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
ToolbarFilter,
|
||||
SearchInput,
|
||||
Button,
|
||||
PaginationVariant,
|
||||
Pagination,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Table,
|
||||
Thead,
|
||||
Tr,
|
||||
Th,
|
||||
Tbody,
|
||||
Td,
|
||||
ThProps,
|
||||
CustomActionsToggleProps,
|
||||
ActionsColumn,
|
||||
IActions,
|
||||
} from '@patternfly/react-table';
|
||||
import { Workspace, WorkspaceState } from '../../../shared/types';
|
||||
import { FilterIcon } from '@patternfly/react-icons';
|
||||
|
||||
export const Workspaces: React.FunctionComponent = () => {
|
||||
/* Mocked workspaces, to be removed after fetching info from backend */
|
||||
const workspaces: Workspace[] = [
|
||||
{
|
||||
name: 'My Jupyter Notebook',
|
||||
namespace: 'namespace1',
|
||||
paused: true,
|
||||
deferUpdates: true,
|
||||
kind: 'jupyter-lab',
|
||||
podTemplate: {
|
||||
volumes: {
|
||||
home: '/home',
|
||||
data: [
|
||||
{
|
||||
pvcName: 'data',
|
||||
mountPath: '/data',
|
||||
readOnly: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
imageConfig: 'jupyterlab_scipy_180',
|
||||
podConfig: 'Small CPU',
|
||||
},
|
||||
status: {
|
||||
activity: {
|
||||
lastActivity: 0,
|
||||
lastUpdate: 0,
|
||||
},
|
||||
pauseTime: 0,
|
||||
pendingRestart: false,
|
||||
podTemplateOptions: {
|
||||
imageConfig: {
|
||||
desired: '',
|
||||
redirectChain: [],
|
||||
},
|
||||
},
|
||||
state: WorkspaceState.Paused,
|
||||
stateMessage: 'It is paused.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'My Other Jupyter Notebook',
|
||||
namespace: 'namespace1',
|
||||
paused: false,
|
||||
deferUpdates: false,
|
||||
kind: 'jupyter-lab',
|
||||
podTemplate: {
|
||||
volumes: {
|
||||
home: '/home',
|
||||
data: [
|
||||
{
|
||||
pvcName: 'data',
|
||||
mountPath: '/data',
|
||||
readOnly: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
options: {
|
||||
imageConfig: 'jupyterlab_scipy_180',
|
||||
podConfig: 'Large CPU',
|
||||
},
|
||||
status: {
|
||||
activity: {
|
||||
lastActivity: 0,
|
||||
lastUpdate: 0,
|
||||
},
|
||||
pauseTime: 0,
|
||||
pendingRestart: false,
|
||||
podTemplateOptions: {
|
||||
imageConfig: {
|
||||
desired: '',
|
||||
redirectChain: [],
|
||||
},
|
||||
},
|
||||
state: WorkspaceState.Running,
|
||||
stateMessage: 'It is running.',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Table columns
|
||||
const columnNames = {
|
||||
name: 'Name',
|
||||
kind: 'Kind',
|
||||
image: 'Image',
|
||||
podConfig: 'Pod Config',
|
||||
state: 'State',
|
||||
homeVol: 'Home Vol',
|
||||
dataVol: 'Data Vol',
|
||||
lastActivity: 'Last Activity',
|
||||
};
|
||||
|
||||
// Filter
|
||||
const [activeAttributeMenu, setActiveAttributeMenu] = React.useState<string>(columnNames.name);
|
||||
const [isAttributeMenuOpen, setIsAttributeMenuOpen] = React.useState(false);
|
||||
const attributeToggleRef = React.useRef<MenuToggleElement | null>(null);
|
||||
const attributeMenuRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const attributeContainerRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const [searchValue, setSearchValue] = React.useState('');
|
||||
|
||||
const searchInput = (
|
||||
<SearchInput
|
||||
placeholder="Filter by name"
|
||||
value={searchValue}
|
||||
onChange={(_event, value) => onSearchChange(value)}
|
||||
onClear={() => onSearchChange('')}
|
||||
/>
|
||||
);
|
||||
|
||||
const handleAttributeMenuKeys = (event: KeyboardEvent) => {
|
||||
if (!isAttributeMenuOpen) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
attributeMenuRef.current?.contains(event.target as Node) ||
|
||||
attributeToggleRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
if (event.key === 'Escape' || event.key === 'Tab') {
|
||||
setIsAttributeMenuOpen(!isAttributeMenuOpen);
|
||||
attributeToggleRef.current?.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleAttributeClickOutside = (event: MouseEvent) => {
|
||||
if (isAttributeMenuOpen && !attributeMenuRef.current?.contains(event.target as Node)) {
|
||||
setIsAttributeMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('keydown', handleAttributeMenuKeys);
|
||||
window.addEventListener('click', handleAttributeClickOutside);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleAttributeMenuKeys);
|
||||
window.removeEventListener('click', handleAttributeClickOutside);
|
||||
};
|
||||
}, [isAttributeMenuOpen, attributeMenuRef]);
|
||||
|
||||
const onAttributeToggleClick = (ev: React.MouseEvent) => {
|
||||
ev.stopPropagation(); // Stop handleClickOutside from handling
|
||||
setTimeout(() => {
|
||||
if (attributeMenuRef.current) {
|
||||
const firstElement = attributeMenuRef.current.querySelector('li > button:not(:disabled)');
|
||||
firstElement && (firstElement as HTMLElement).focus();
|
||||
}
|
||||
}, 0);
|
||||
setIsAttributeMenuOpen(!isAttributeMenuOpen);
|
||||
};
|
||||
|
||||
const attributeToggle = (
|
||||
<MenuToggle
|
||||
ref={attributeToggleRef}
|
||||
onClick={onAttributeToggleClick}
|
||||
isExpanded={isAttributeMenuOpen}
|
||||
icon={<FilterIcon />}
|
||||
>
|
||||
{activeAttributeMenu}
|
||||
</MenuToggle>
|
||||
);
|
||||
|
||||
const attributeMenu = (
|
||||
<Menu
|
||||
ref={attributeMenuRef}
|
||||
onSelect={(_ev, itemId) => {
|
||||
setActiveAttributeMenu(itemId?.toString());
|
||||
setIsAttributeMenuOpen(!isAttributeMenuOpen);
|
||||
}}
|
||||
>
|
||||
<MenuContent>
|
||||
<MenuList>
|
||||
<MenuItem itemId="Name">Name</MenuItem>
|
||||
<MenuItem itemId="Kind">Kind</MenuItem>
|
||||
<MenuItem itemId="Image">Image</MenuItem>
|
||||
<MenuItem itemId="Pod Config">Pod Config</MenuItem>
|
||||
<MenuItem itemId="State">State</MenuItem>
|
||||
<MenuItem itemId="Home Vol">Home Vol</MenuItem>
|
||||
<MenuItem itemId="Data Vol">Data Vol</MenuItem>
|
||||
<MenuItem itemId="Last Activity">Last Activity</MenuItem>
|
||||
</MenuList>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const attributeDropdown = (
|
||||
<div ref={attributeContainerRef}>
|
||||
<Popper
|
||||
trigger={attributeToggle}
|
||||
triggerRef={attributeToggleRef}
|
||||
popper={attributeMenu}
|
||||
popperRef={attributeMenuRef}
|
||||
appendTo={attributeContainerRef.current || undefined}
|
||||
isVisible={isAttributeMenuOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const toolbar = (
|
||||
<Toolbar
|
||||
id="attribute-search-filter-toolbar"
|
||||
clearAllFilters={() => {
|
||||
setSearchValue('');
|
||||
}}
|
||||
>
|
||||
<ToolbarContent>
|
||||
<ToolbarToggleGroup toggleIcon={<FilterIcon />} breakpoint="xl">
|
||||
<ToolbarGroup variant="filter-group">
|
||||
<ToolbarItem>{attributeDropdown}</ToolbarItem>
|
||||
<ToolbarFilter
|
||||
labels={searchValue !== '' ? [searchValue] : ([] as string[])}
|
||||
deleteLabel={() => setSearchValue('')}
|
||||
deleteLabelGroup={() => setSearchValue('')}
|
||||
categoryName={activeAttributeMenu}
|
||||
>
|
||||
{searchInput}
|
||||
</ToolbarFilter>
|
||||
<Button variant="primary" ouiaId="Primary">
|
||||
Create Workspace
|
||||
</Button>
|
||||
</ToolbarGroup>
|
||||
</ToolbarToggleGroup>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
);
|
||||
|
||||
const onSearchChange = (value: string) => {
|
||||
setSearchValue(value);
|
||||
};
|
||||
|
||||
const onFilter = (workspace: Workspace) => {
|
||||
// Search name with search value
|
||||
let searchValueInput: RegExp;
|
||||
try {
|
||||
searchValueInput = new RegExp(searchValue, 'i');
|
||||
} catch (err) {
|
||||
searchValueInput = new RegExp(searchValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
|
||||
}
|
||||
|
||||
return (
|
||||
searchValue === '' ||
|
||||
(activeAttributeMenu === 'Name' && workspace.name.search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'Kind' && workspace.kind.search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'Image' &&
|
||||
workspace.options.imageConfig.search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'Pod Config' &&
|
||||
workspace.options.podConfig.search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'State' &&
|
||||
WorkspaceState[workspace.status.state].search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'Home Vol' &&
|
||||
workspace.podTemplate.volumes.home.search(searchValueInput) >= 0) ||
|
||||
(activeAttributeMenu === 'Data Vol' &&
|
||||
workspace.podTemplate.volumes.data.some(
|
||||
(dataVol) =>
|
||||
dataVol.pvcName.search(searchValueInput) >= 0 ||
|
||||
dataVol.mountPath.search(searchValueInput) >= 0,
|
||||
))
|
||||
);
|
||||
};
|
||||
const filteredWorkspaces = workspaces.filter(onFilter);
|
||||
|
||||
// Column sorting
|
||||
|
||||
const [activeSortIndex, setActiveSortIndex] = React.useState<number | null>(null);
|
||||
const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null);
|
||||
|
||||
const getSortableRowValues = (workspace: Workspace): (string | number)[] => {
|
||||
const { name, kind, image, podConfig, state, homeVol, dataVol, lastActivity } = {
|
||||
name: workspace.name,
|
||||
kind: workspace.kind,
|
||||
image: workspace.options.imageConfig,
|
||||
podConfig: workspace.options.podConfig,
|
||||
state: WorkspaceState[workspace.status.state],
|
||||
homeVol: workspace.podTemplate.volumes.home,
|
||||
dataVol: workspace.podTemplate.volumes.data[0].pvcName || '',
|
||||
lastActivity: workspace.status.activity.lastActivity,
|
||||
};
|
||||
return [name, kind, image, podConfig, state, homeVol, dataVol, lastActivity];
|
||||
};
|
||||
|
||||
let sortedWorkspaces = filteredWorkspaces;
|
||||
if (activeSortIndex !== null) {
|
||||
sortedWorkspaces = workspaces.sort((a, b) => {
|
||||
const aValue = getSortableRowValues(a)[activeSortIndex];
|
||||
const bValue = getSortableRowValues(b)[activeSortIndex];
|
||||
if (typeof aValue === 'number') {
|
||||
// Numeric sort
|
||||
if (activeSortDirection === 'asc') {
|
||||
return (aValue as number) - (bValue as number);
|
||||
}
|
||||
return (bValue as number) - (aValue as number);
|
||||
} else {
|
||||
// String sort
|
||||
if (activeSortDirection === 'asc') {
|
||||
return (aValue as string).localeCompare(bValue as string);
|
||||
}
|
||||
return (bValue as string).localeCompare(aValue as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const getSortParams = (columnIndex: number): ThProps['sort'] => ({
|
||||
sortBy: {
|
||||
index: activeSortIndex || 0,
|
||||
direction: activeSortDirection || 'asc',
|
||||
defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc'
|
||||
},
|
||||
onSort: (_event, index, direction) => {
|
||||
setActiveSortIndex(index);
|
||||
setActiveSortDirection(direction);
|
||||
},
|
||||
columnIndex,
|
||||
});
|
||||
|
||||
// Actions
|
||||
|
||||
const defaultActions = (workspace: Workspace): IActions =>
|
||||
[
|
||||
{
|
||||
title: 'Edit',
|
||||
onClick: () => console.log(`Clicked on edit, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
title: 'Delete',
|
||||
onClick: () => console.log(`Clicked on delete, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
isSeparator: true,
|
||||
},
|
||||
{
|
||||
title: 'Start/restart',
|
||||
onClick: () => console.log(`Clicked on start/restart, on row ${workspace.name}`),
|
||||
},
|
||||
{
|
||||
title: 'Stop',
|
||||
onClick: () => console.log(`Clicked on stop, on row ${workspace.name}`),
|
||||
},
|
||||
] as IActions;
|
||||
|
||||
// States
|
||||
|
||||
const stateColors: (
|
||||
| 'blue'
|
||||
| 'teal'
|
||||
| 'green'
|
||||
| 'orange'
|
||||
| 'purple'
|
||||
| 'red'
|
||||
| 'orangered'
|
||||
| 'grey'
|
||||
| 'yellow'
|
||||
)[] = ['green', 'orange', 'yellow', 'blue', 'red', 'purple'];
|
||||
|
||||
// Pagination
|
||||
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [perPage, setPerPage] = React.useState(10);
|
||||
|
||||
const onSetPage = (
|
||||
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
|
||||
newPage: number,
|
||||
) => {
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const onPerPageSelect = (
|
||||
_event: React.MouseEvent | React.KeyboardEvent | MouseEvent,
|
||||
newPerPage: number,
|
||||
newPage: number,
|
||||
) => {
|
||||
setPerPage(newPerPage);
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Title headingLevel="h1">Kubeflow Workspaces</Title>
|
||||
<p>View your existing workspaces or create new workspaces.</p>
|
||||
{toolbar}
|
||||
<Table aria-label="Sortable table" ouiaId="SortableTable">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th sort={getSortParams(0)}>{columnNames.name}</Th>
|
||||
<Th sort={getSortParams(1)}>{columnNames.kind}</Th>
|
||||
<Th sort={getSortParams(2)}>{columnNames.image}</Th>
|
||||
<Th sort={getSortParams(3)}>{columnNames.podConfig}</Th>
|
||||
<Th sort={getSortParams(4)}>{columnNames.state}</Th>
|
||||
<Th sort={getSortParams(5)}>{columnNames.homeVol}</Th>
|
||||
<Th sort={getSortParams(6)}>{columnNames.dataVol}</Th>
|
||||
<Th sort={getSortParams(7)}>{columnNames.lastActivity}</Th>
|
||||
<Th screenReaderText="Primary action"></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{sortedWorkspaces.map((workspace, rowIndex) => (
|
||||
<Tr key={rowIndex}>
|
||||
<Td dataLabel={columnNames.name}>{workspace.name}</Td>
|
||||
<Td dataLabel={columnNames.kind}>{workspace.kind}</Td>
|
||||
<Td dataLabel={columnNames.image}>{workspace.options.imageConfig}</Td>
|
||||
<Td dataLabel={columnNames.podConfig}>{workspace.options.podConfig}</Td>
|
||||
<Td dataLabel={columnNames.state}>
|
||||
<Label color={stateColors[workspace.status.state]}>
|
||||
{WorkspaceState[workspace.status.state]}
|
||||
</Label>
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.homeVol}>{workspace.podTemplate.volumes.home}</Td>
|
||||
<Td dataLabel={columnNames.dataVol}>
|
||||
{workspace.podTemplate.volumes.data[0].pvcName || ''}
|
||||
</Td>
|
||||
<Td dataLabel={columnNames.lastActivity}>
|
||||
<Timestamp
|
||||
date={new Date(workspace.status.activity.lastActivity)}
|
||||
tooltip={{ variant: TimestampTooltipVariant.default }}
|
||||
>
|
||||
1 hour ago
|
||||
</Timestamp>
|
||||
</Td>
|
||||
<Td isActionCell>
|
||||
<ActionsColumn items={defaultActions(workspace)} />
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
<Pagination
|
||||
itemCount={333}
|
||||
widgetId="bottom-example"
|
||||
perPage={perPage}
|
||||
page={page}
|
||||
variant={PaginationVariant.bottom}
|
||||
onSetPage={onSetPage}
|
||||
onPerPageSelect={onPerPageSelect}
|
||||
/>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,12 @@
|
|||
import * as React from 'react';
|
||||
import { ExclamationTriangleIcon } from '@patternfly/react-icons';
|
||||
import { Button, EmptyState, EmptyStateBody, EmptyStateFooter, PageSection } from '@patternfly/react-core';
|
||||
import {
|
||||
Button,
|
||||
EmptyState,
|
||||
EmptyStateBody,
|
||||
EmptyStateFooter,
|
||||
PageSection,
|
||||
} from '@patternfly/react-core';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const NotFound: React.FunctionComponent = () => {
|
||||
|
@ -11,9 +17,7 @@ const NotFound: React.FunctionComponent = () => {
|
|||
navigate('/');
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick}>Take me home</Button>
|
||||
);
|
||||
return <Button onClick={handleClick}>Take me home</Button>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -21,9 +25,11 @@ const NotFound: React.FunctionComponent = () => {
|
|||
<EmptyState titleText="404 Page not found" variant="full" icon={ExclamationTriangleIcon}>
|
||||
<EmptyStateBody>
|
||||
We didn't find a page that matches the address you navigated to.
|
||||
</EmptyStateBody><EmptyStateFooter>
|
||||
<GoHomeBtn />
|
||||
</EmptyStateFooter></EmptyState>
|
||||
</EmptyStateBody>
|
||||
<EmptyStateFooter>
|
||||
<GoHomeBtn />
|
||||
</EmptyStateFooter>
|
||||
</EmptyState>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,5 +10,5 @@ root.render(
|
|||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</React.StrictMode>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
export interface WorkspaceIcon {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceLogo {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceKind {
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
hidden: boolean;
|
||||
deprecated: boolean;
|
||||
deprecationWarning: string;
|
||||
icon: WorkspaceIcon;
|
||||
logo: WorkspaceLogo;
|
||||
}
|
||||
|
||||
export enum WorkspaceState {
|
||||
Running,
|
||||
Terminating,
|
||||
Paused,
|
||||
Pending,
|
||||
Error,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
export interface WorkspaceStatus {
|
||||
activity: {
|
||||
lastActivity: number;
|
||||
lastUpdate: number;
|
||||
};
|
||||
pauseTime: number;
|
||||
pendingRestart: boolean;
|
||||
podTemplateOptions: {
|
||||
imageConfig: {
|
||||
desired: string;
|
||||
redirectChain: {
|
||||
source: string;
|
||||
target: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
state: WorkspaceState;
|
||||
stateMessage: string;
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
name: string;
|
||||
namespace: string;
|
||||
paused: boolean;
|
||||
deferUpdates: boolean;
|
||||
kind: string;
|
||||
podTemplate: {
|
||||
volumes: {
|
||||
home: string;
|
||||
data: {
|
||||
pvcName: string;
|
||||
mountPath: string;
|
||||
readOnly: boolean;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
options: {
|
||||
imageConfig: string;
|
||||
podConfig: string;
|
||||
};
|
||||
status: WorkspaceStatus;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
declare module "*.png";
|
||||
declare module "*.jpg";
|
||||
declare module "*.jpeg";
|
||||
declare module "*.gif";
|
||||
declare module "*.svg";
|
||||
declare module "*.css";
|
||||
declare module "*.wav";
|
||||
declare module "*.mp3";
|
||||
declare module "*.m4a";
|
||||
declare module "*.rdf";
|
||||
declare module "*.ttl";
|
||||
declare module "*.pdf";
|
||||
declare module '*.png';
|
||||
declare module '*.jpg';
|
||||
declare module '*.jpeg';
|
||||
declare module '*.gif';
|
||||
declare module '*.svg';
|
||||
declare module '*.css';
|
||||
declare module '*.wav';
|
||||
declare module '*.mp3';
|
||||
declare module '*.m4a';
|
||||
declare module '*.rdf';
|
||||
declare module '*.ttl';
|
||||
declare module '*.pdf';
|
||||
|
|
|
@ -5,10 +5,7 @@
|
|||
"outDir": "dist",
|
||||
"module": "esnext",
|
||||
"target": "ES6",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom"
|
||||
],
|
||||
"lib": ["es6", "dom"],
|
||||
"sourceMap": true,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "node",
|
||||
|
@ -27,13 +24,6 @@
|
|||
"importHelpers": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.jsx",
|
||||
"**/*.js",
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue