Sync UI template with latest updates

This commit is contained in:
Tan Nhu 2022-08-12 12:09:57 -07:00
parent b5e426a177
commit 66fea2a730
55 changed files with 11589 additions and 2455 deletions

View File

@ -0,0 +1,48 @@
const packageJSON = require('../package.json');
const { pick, omit, mapValues } = require('lodash');
/**
* These packages must be stricly shared with exact versions
*/
const ExactSharedPackages = [
'react',
'react-dom',
'react-router-dom',
'@harness/use-modal',
'@blueprintjs/core',
'@blueprintjs/select',
'@blueprintjs/datetime',
'restful-react',
'@harness/monaco-yaml',
'monaco-editor',
'monaco-editor-core',
'monaco-languages',
'monaco-plugin-helpers',
'react-monaco-editor'
]
/**
* @type {import('webpack').ModuleFederationPluginOptions}
*/
module.exports = {
name: 'governance',
filename: 'remoteEntry.js',
library: {
type: 'var',
name: 'governance'
},
exposes: {
'./App': './src/App.tsx',
'./EvaluationModal': './src/modals/EvaluationModal/EvaluationModal.tsx',
'./PipelineGovernanceView': './src/views/PipelineGovernanceView/PipelineGovernanceView.tsx',
'./EvaluationView': './src/views/EvaluationView/EvaluationView.tsx',
'./PolicySetWizard': './src/pages/PolicySets/components/PolicySetWizard.tsx'
},
shared: {
formik: packageJSON.dependencies['formik'],
...mapValues(pick(packageJSON.dependencies, ExactSharedPackages), version => ({
singleton: true,
requiredVersion: version
}))
}
};

View File

@ -0,0 +1,168 @@
const path = require('path');
const webpack = require('webpack')
const {
container: { ModuleFederationPlugin },
DefinePlugin
} = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const GenerateStringTypesPlugin = require('../scripts/webpack/GenerateStringTypesPlugin').GenerateStringTypesPlugin
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
const moduleFederationConfig = require('./moduleFederation.config');
const CONTEXT = process.cwd();
const DEV = process.env.NODE_ENV === 'development'
const ON_PREM = `${process.env.ON_PREM}` === 'true'
module.exports = {
target: 'web',
context: CONTEXT,
stats: {
modules: false,
children: false
},
output: {
publicPath: 'auto',
filename: DEV ? 'static/[name].js' : 'static/[name].[contenthash:6].js',
chunkFilename: DEV ? 'static/[name].[id].js' : 'static/[name].[id].[contenthash:6].js',
pathinfo: false
},
module: {
rules: [
{
test: /\.m?js$/,
include: /node_modules/,
type: 'javascript/auto'
},
{
test: /\.(j|t)sx?$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
]
},
{
test: /\.module\.scss$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
{
loader: '@harness/css-types-loader',
options: {
prettierConfig: CONTEXT
}
},
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
mode: 'local',
localIdentName: DEV ? '[name]_[local]_[hash:base64:6]' : '[hash:base64:6]',
exportLocalsConvention: 'camelCaseOnly'
}
}
},
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: [path.join(CONTEXT, 'src')]
},
sourceMap: false,
implementation: require('sass')
}
}
]
},
{
test: /(?<!\.module)\.scss$/,
exclude: /node_modules/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: false
}
},
{
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: [path.join(CONTEXT, 'src')]
},
implementation: require('sass')
}
}
]
},
{
test: /\.(jpg|jpeg|png|svg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 2000,
fallback: 'file-loader'
}
}
]
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.ttf$/,
loader: 'file-loader'
},
{
test: /\.ya?ml$/,
type: 'json',
use: [
{
loader: 'yaml-loader'
}
]
},
{
test: /\.gql$/,
type: 'asset/source'
},
{
test: /\.(mp4)$/,
use: [
{
loader: 'file-loader'
}
]
}
]
},
resolve: {
extensions: ['.mjs', '.js', '.ts', '.tsx', '.json', '.ttf', '.scss'],
plugins: [
new TsconfigPathsPlugin()]
},
plugins: [
new ModuleFederationPlugin(moduleFederationConfig),
new DefinePlugin({
'process.env': '{}', // required for @blueprintjs/core
__DEV__: DEV,
__ON_PREM__: ON_PREM
}),
new GenerateStringTypesPlugin(),
new RetryChunkLoadPlugin({
maxRetries: 2
}),
]
};

75
web/config/webpack.dev.js Normal file
View File

@ -0,0 +1,75 @@
const path = require('path');
const util = require('util');
const fs = require('fs');
require('dotenv').config();
const { merge } = require('webpack-merge');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin, WatchIgnorePlugin, container: { ModuleFederationPlugin }} = require('webpack');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const commonConfig = require('./webpack.common');
const baseUrl = process.env.BASE_URL ?? 'https://qa.harness.io/gateway'
const targetLocalHost = JSON.parse(process.env.TARGET_LOCALHOST || 'true')
const ON_PREM = `${process.env.ON_PREM}` === 'true'
const DEV = process.env.NODE_ENV === 'development'
const devConfig = {
mode: 'development',
entry: './src/index.tsx',
devtool: 'cheap-module-source-map',
cache: { type: 'filesystem' },
output: {
filename: '[name].js',
chunkFilename: '[name].[id].js'
},
devServer: {
hot: true,
host: "localhost",
historyApiFallback: true,
port: 3000,
proxy: {
'/api': {
target: targetLocalHost ? 'http://localhost:3001' : baseUrl,
logLevel: 'debug',
secure: false,
changeOrigin: true
}
}
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].[id].css'
}),
new HTMLWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
minify: false,
templateParameters: {
__DEV__: DEV,
__ON_PREM__: ON_PREM
}
}),
new DefinePlugin({
'process.env': '{}', // required for @blueprintjs/core
__DEV__: DEV
}),
new MonacoWebpackPlugin({
// available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
languages: ['yaml', 'json']
}),
// new ForkTsCheckerWebpackPlugin()
// new WatchIgnorePlugin({
// paths: [/node_modules(?!\/@wings-software)/, /\.d\.ts$/]
// }),
]
};
console.table({ baseUrl, targetLocalHost })
module.exports = merge(commonConfig, devConfig);

View File

@ -0,0 +1,52 @@
const { merge } = require('webpack-merge');
const HTMLWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const JSONGeneratorPlugin = require('@harness/jarvis/lib/webpack/json-generator-plugin').default;
const { DefinePlugin } = require('webpack');
const commonConfig = require('./webpack.common');
const ON_PREM = `${process.env.ON_PREM}` === 'true'
const prodConfig = {
mode: 'production',
devtool: 'hidden-source-map',
output: {
filename: '[name].[contenthash:6].js',
chunkFilename: '[name].[id].[contenthash:6].js'
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:6].css',
chunkFilename: '[name].[id].[contenthash:6].css'
}),
new JSONGeneratorPlugin({
content: {
version: require('../package.json').version,
gitCommit: process.env.GIT_COMMIT,
gitBranch: process.env.GIT_BRANCH
},
filename: 'version.json'
}),
new CircularDependencyPlugin({
exclude: /node_modules/,
failOnError: true
}),
new HTMLWebpackPlugin({
template: 'src/index.html',
filename: 'index.html',
minify: false,
templateParameters: {
__ON_PREM__: ON_PREM
}
}),
]
};
module.exports = merge(commonConfig, prodConfig);

View File

@ -0,0 +1,8 @@
describe('dashboard', () => {
it('load the dashboard', () => {
cy.visit('/')
cy.contains('In Effect')
cy.contains('Policy Evaluations')
cy.contains('Failures Recorded')
})
})

View File

@ -0,0 +1,9 @@
// disabling at the moment because of logic around Evaluations tab being removed in standalone mode (account in NG equivalent)
// describe('evaluations', () => {
// it('load the table', () => {
// cy.visit('/')
// cy.contains('Evaluations').click()
// cy.contains('Policy evaluations are created when policy sets are enforced on your Harness entities.')
// })
// })

View File

@ -0,0 +1,7 @@
describe('policies', () => {
it('load the table', () => {
cy.visit('/')
cy.contains('Policies').click()
cy.get('[class="TableV2--row TableV2--card TableV2--clickable"]').should('have.length', 12)
})
})

View File

@ -0,0 +1,7 @@
describe('policy sets', () => {
it('load the table', () => {
cy.visit('/')
cy.contains('Policy Set').click()
cy.contains('A harness policy set allows you to group policies and configure where they will be enforced.')
})
})

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -10,40 +10,7 @@ package web
import ( import (
"embed" "embed"
"io/fs"
"net/http"
"path/filepath"
) )
//go:embed dist/* //go:embed dist/*
var content embed.FS var UI embed.FS
// Handler returns an http.HandlerFunc that servers the
// static content from the embedded file system.
func Handler() http.HandlerFunc {
// Load the files subdirectory
fs, err := fs.Sub(content, "dist")
if err != nil {
panic(err)
}
// Create an http.FileServer to serve the
// contents of the files subdiretory.
handler := http.FileServer(http.FS(fs))
// Create an http.HandlerFunc that wraps the
// http.FileServer to always load the index.html
// file if a directory path is being requested.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// because this is a single page application,
// we need to always load the index.html file
// in the root of the project, unless the path
// points to a file with an extension (css, js, etc)
if filepath.Ext(r.URL.Path) == "" {
// HACK: alter the path to point to the
// root of the project.
r.URL.Path = "/"
}
// and finally server the file.
handler.ServeHTTP(w, r)
})
}

View File

@ -1,5 +1,5 @@
{ {
"name": "sample-module", "name": "ui-template",
"description": "Harness Inc", "description": "Harness Inc",
"version": "0.0.1", "version": "0.0.1",
"author": "Harness Inc", "author": "Harness Inc",
@ -8,14 +8,14 @@
"homepage": "http://harness.io/", "homepage": "http://harness.io/",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/drone/sample-module.git" "url": "https://github.com/wings-software/ui-template.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/sample-module/sample-module/issues" "url": "https://github.com/wings-software/ui-template/issues"
}, },
"keywords": [], "keywords": [],
"scripts": { "scripts": {
"dev": "NODE_ENV=development webpack serve --progress", "dev": "webpack serve --config config/webpack.dev.js",
"test": "jest src --silent", "test": "jest src --silent",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"lint": "eslint --rulesdir ./scripts/eslint-rules --ext .ts --ext .tsx src", "lint": "eslint --rulesdir ./scripts/eslint-rules --ext .ts --ext .tsx src",
@ -24,60 +24,130 @@
"services": "npm-run-all services:*", "services": "npm-run-all services:*",
"services:pm": "restful-react import --config restful-react.config.js pm", "services:pm": "restful-react import --config restful-react.config.js pm",
"postservices": "prettier --write src/services/**/*.tsx", "postservices": "prettier --write src/services/**/*.tsx",
"build": "npm run clean; webpack --mode production", "build": "rm -rf dist && webpack --config config/webpack.prod.js",
"coverage": "npm test --coverage", "coverage": "npm test --coverage",
"setup-github-registry": "sh scripts/setup-github-registry.sh", "setup-github-registry": "sh scripts/setup-github-registry.sh",
"strings": "npm-run-all strings:*", "strings": "npm-run-all strings:*",
"strings:genTypes": "node scripts/strings/generateTypesCli.mjs", "strings:genTypes": "node scripts/strings/generateTypesCli.mjs",
"fmt": "prettier --write \"./src/**/*.{ts,tsx,css,scss}\"", "fmt": "prettier --write \"./src/**/*.{ts,tsx,css,scss}\"",
"micro:watch": "nodemon --watch 'src/**/*' -e ts,tsx,html,scss,svg,yaml --exec 'npm-run-all' -- micro:build micro:serve", "checks": "npm run typecheck; npm run lint; npm run test"
"micro:build": "webpack --mode production",
"micro:serve": "serve ./dist -l 3000"
}, },
"dependencies": { "dependencies": {
"@blueprintjs/core": "3.26.1", "@blueprintjs/core": "3.26.1",
"@blueprintjs/datetime": "3.13.0", "@blueprintjs/datetime": "3.13.0",
"@blueprintjs/select": "3.12.3", "@blueprintjs/select": "3.12.3",
"@harness/uicore": "^1.23.0", "@emotion/core": "^10.0.28",
"anser": "^2.1.0", "@emotion/styled": "^10.0.27",
"classnames": "^2.3.1", "@harness/design-system": "1.0.0",
"@harness/icons": "^1.27.0",
"@harness/monaco-yaml": ">=1.0.0",
"@harness/ng-tooltip": ">=1.30.68",
"@harness/telemetry": ">=1.0.37",
"@harness/uicore": "3.70.0",
"@harness/use-modal": ">=1.1.0",
"@popperjs/core": "^2.4.2",
"@projectstorm/react-diagrams-core": "^6.6.0",
"@urql/exchange-request-policy": "^0.1.3",
"anser": "^2.0.1",
"classnames": "^2.2.6",
"clipboard-copy": "^3.1.0", "clipboard-copy": "^3.1.0",
"formik": "1.5.8", "closest": "^0.0.1",
"copy-to-clipboard": "^3.3.1",
"cron-validator": "^1.2.1",
"cronstrue": "^1.114.0",
"event-source-polyfill": "^1.0.22",
"formik": "2.2.9",
"highcharts": "9.1.0",
"highcharts-react-official": "3.0.0",
"idb": "^5.0.4",
"immer": "^9.0.6", "immer": "^9.0.6",
"jsonc-parser": "^2.0.2",
"lodash-es": "^4.17.15", "lodash-es": "^4.17.15",
"marked": "^3.0.8", "marked": "^4.0.12",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"ml-matrix": "^6.5.0",
"moment": "^2.25.3", "moment": "^2.25.3",
"moment-range": "^4.0.2",
"monaco-editor": "^0.19.2", "monaco-editor": "^0.19.2",
"monaco-editor-core": "0.15.5",
"monaco-languages": "1.6.0",
"monaco-plugin-helpers": "^1.0.2",
"p-debounce": "^3.0.1",
"qs": "^6.9.4", "qs": "^6.9.4",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^13.0.0",
"react-contenteditable": "^3.3.5",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-draggable": "^4.4.2", "react-draggable": "^4.4.2",
"react-router-dom": "5.2.0", "react-lottie-player": "^1.4.0",
"react-monaco-editor": "^0.34.0",
"react-popper": "^2.2.3",
"react-qr-code": "^1.1.1",
"react-router-dom": "^5.2.0",
"react-split-pane": "^0.1.92",
"react-table": "^7.1.0", "react-table": "^7.1.0",
"react-table-sticky": "^1.1.3",
"react-timeago": "^4.4.0",
"react-virtuoso": "^1.10.2",
"restful-react": "15.6.0", "restful-react": "15.6.0",
"swr": "^0.5.4", "secure-web-storage": "^1.0.2",
"yaml": "^1.10.0" "urql": "^2.0.3",
"uuid": "^8.3.0",
"vscode-languageserver-types": "3.15.1",
"webpack-retry-chunk-load-plugin": "^3.1.0",
"yaml": "^1.10.0",
"yup": "^0.29.1"
}, },
"devDependencies": { "devDependencies": {
"@harness/css-types-loader": "^3.1.0", "@babel/core": "^7.13.15",
"@harness/jarvis": "0.12.0", "@emotion/react": "^11.4.0",
"@graphql-codegen/cli": "^1.21.2",
"@graphql-codegen/typescript": "^1.21.1",
"@graphql-codegen/typescript-operations": "^1.17.15",
"@graphql-codegen/typescript-urql": "^2.0.6",
"@harness/css-types-loader": "2.0.2",
"@harness/jarvis": "^0.12.0",
"@istanbuljs/nyc-config-typescript": "^1.0.1",
"@stoplight/prism-cli": "^4.3.1",
"@stoplight/prism-http": "^4.3.1",
"@storybook/addon-actions": "^6.3.1",
"@storybook/addon-docs": "^6.3.1",
"@storybook/addon-essentials": "^6.3.1",
"@storybook/addon-links": "^6.3.1",
"@storybook/builder-webpack5": "^6.3.1",
"@storybook/manager-webpack5": "^6.3.1",
"@storybook/react": "^6.3.1",
"@testing-library/jest-dom": "^5.12.0", "@testing-library/jest-dom": "^5.12.0",
"@testing-library/react": "^10.0.3", "@testing-library/react": "^10.0.3",
"@testing-library/react-hooks": "5", "@testing-library/react-hooks": "5",
"@testing-library/user-event": "^10.3.1", "@testing-library/user-event": "^10.3.1",
"@types/classnames": "^2.2.10", "@types/classnames": "^2.2.10",
"@types/jest": "^26.0.15",
"@types/lodash-es": "^4.17.3", "@types/lodash-es": "^4.17.3",
"@types/masonry-layout": "^4.2.1",
"@types/mustache": "^4.0.1", "@types/mustache": "^4.0.1",
"@types/node": "^16.4.10",
"@types/path-to-regexp": "^1.7.0", "@types/path-to-regexp": "^1.7.0",
"@types/qs": "^6.9.4", "@types/qs": "^6.9.4",
"@types/query-string": "^6.3.0",
"@types/react": "^17.0.3", "@types/react": "^17.0.3",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.3", "@types/react-dom": "^17.0.3",
"@types/react-monaco-editor": "^0.16.0",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.7",
"@types/react-table": "^7.0.18", "@types/react-table": "^7.0.18",
"@types/react-timeago": "^4.1.1",
"@types/testing-library__react-hooks": "^3.2.0", "@types/testing-library__react-hooks": "^3.2.0",
"@types/testing-library__user-event": "^4.1.1",
"@types/uuid": "^8.3.0",
"@types/yup": "^0.29.0",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0", "@typescript-eslint/parser": "^4.22.0",
"@urql/devtools": "^2.0.3",
"@zerollup/ts-transform-paths": "^1.7.18",
"assert": "^2.0.0",
"babel-loader": "^8.2.2",
"cache-loader": "^4.1.0",
"case": "^1.6.3", "case": "^1.6.3",
"circular-dependency-plugin": "^5.2.2", "circular-dependency-plugin": "^5.2.2",
"css-loader": "^6.3.0", "css-loader": "^6.3.0",
@ -89,12 +159,23 @@
"eslint-plugin-jest": "^24.3.6", "eslint-plugin-jest": "^24.3.6",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-hooks": "^4.2.0",
"express": "^4.17.1",
"external-remotes-plugin": "^1.0.0",
"fake-indexeddb": "^3.1.2",
"fast-json-stable-stringify": "^2.1.0", "fast-json-stable-stringify": "^2.1.0",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.1", "fork-ts-checker-webpack-plugin": "^6.2.1",
"glob": "^7.1.6", "glob": "^7.1.6",
"graphql": "^15.5.0",
"html-webpack-plugin": "^5.3.1", "html-webpack-plugin": "^5.3.1",
"https": "^1.0.0",
"husky": "^6.0.0",
"identity-obj-proxy": "^3.0.0",
"ignore-loader": "^0.1.2",
"istanbul-lib-coverage": "^3.0.0",
"jest": "^26.2.0", "jest": "^26.2.0",
"jest-canvas-mock": "^2.3.0",
"jest-junit": "^12.0.0",
"lighthouse": "^6.5.0", "lighthouse": "^6.5.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^2.4.2", "mini-css-extract-plugin": "^2.4.2",
@ -102,22 +183,30 @@
"mustache": "^4.0.1", "mustache": "^4.0.1",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"null-loader": "^4.0.1",
"nyc": "^15.1.0",
"patch-package": "^6.4.7",
"path-to-regexp": "^6.1.0", "path-to-regexp": "^6.1.0",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.3.2", "prettier": "^2.3.2",
"react-test-renderer": "^17.0.2", "react-test-renderer": "^17.0.2",
"sass": "^1.32.8", "sass": "^1.32.8",
"sass-loader": "^12.1.0", "sass-loader": "^12.1.0",
"serve": "^13.0.2", "serve": "^13.0.2",
"source-map-support": "^0.5.20",
"style-loader": "^3.3.0", "style-loader": "^3.3.0",
"ts-jest": "^26.5.5", "ts-jest": "^26.5.5",
"ts-loader": "^9.2.6", "ts-loader": "^9.2.6",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1", "tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.58.0", "webpack": "^5.58.0",
"webpack-bugsnag-plugins": "^1.8.0",
"webpack-bundle-analyzer": "^4.4.0", "webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.9.1", "webpack-cli": "^4.9.0",
"webpack-dev-server": "^4.6.0", "webpack-dev-server": "^4.3.1",
"worker-loader": "^3.0.8",
"yaml-loader": "^0.6.0" "yaml-loader": "^0.6.0"
}, },
"resolutions": { "resolutions": {
@ -126,7 +215,11 @@
"@types/testing-library__react": "^10.0.0", "@types/testing-library__react": "^10.0.0",
"@types/testing-library__dom": "^7.0.0", "@types/testing-library__dom": "^7.0.0",
"anser": "2.0.1", "anser": "2.0.1",
"create-react-context": "0.3.0" "create-react-context": "0.3.0",
"@blueprintjs/core": "3.26.1",
"@blueprintjs/datetime": "3.13.0",
"@blueprintjs/icons": "3.16.0",
"@blueprintjs/select": "3.12.3"
}, },
"engines": { "engines": {
"node": ">=14.16.0" "node": ">=14.16.0"

View File

@ -6,12 +6,11 @@ const customGenerator = require('./scripts/swagger-custom-generator.js')
module.exports = { module.exports = {
pm: { pm: {
output: 'src/services/pm/index.tsx', output: 'src/services/policy-mgmt/index.tsx',
file: 'src/services/pm/swagger.json', file: '../design/gen/http/openapi3.json',
transformer: 'scripts/swagger-transform.js', customImport: `import { getConfigNew } from "../config";`,
customImport: `import { getConfig } from "../config";`,
customProps: { customProps: {
base: `{getConfig("pm/api/v1")}` base: `{getConfigNew("pm")}`
} }
} }
} }

View File

@ -1,18 +1,19 @@
import React, { useEffect, useState, useCallback } from 'react' import React, { useEffect, useState, useCallback } from 'react'
import { RestfulProvider } from 'restful-react' import { RestfulProvider } from 'restful-react'
import { TooltipContextProvider, ModalProvider } from '@harness/uicore' import { TooltipContextProvider } from '@harness/uicore'
import { ModalProvider } from '@harness/use-modal'
import { FocusStyleManager } from '@blueprintjs/core' import { FocusStyleManager } from '@blueprintjs/core'
import { tooltipDictionary } from '@harness/ng-tooltip'
import AppErrorBoundary from 'framework/AppErrorBoundary/AppErrorBoundary' import AppErrorBoundary from 'framework/AppErrorBoundary/AppErrorBoundary'
import { useAPIToken } from 'hooks/useAPIToken'
import { AppContextProvider } from 'AppContext' import { AppContextProvider } from 'AppContext'
import { setBaseRouteInfo } from 'RouteUtils' import { setBaseRouteInfo } from 'RouteUtils'
import type { AppProps } from 'AppProps' import type { AppProps } from 'AppProps'
import { buildResfulReactRequestOptions, handle401 } from 'AppUtils' import { buildResfulReactRequestOptions, handle401 } from 'AppUtils'
import { RouteDestinations } from 'RouteDestinations' import { RouteDestinations } from 'RouteDestinations'
import { useAPIToken } from 'hooks/useAPIToken'
import { languageLoader } from './framework/strings/languageLoader' import { languageLoader } from './framework/strings/languageLoader'
import type { LanguageRecord } from './framework/strings/languageLoader' import type { LanguageRecord } from './framework/strings/languageLoader'
import { StringsContextProvider } from './framework/strings/StringsContextProvider' import { StringsContextProvider } from './framework/strings/StringsContextProvider'
import './App.scss'
FocusStyleManager.onlyShowFocusOnTabs() FocusStyleManager.onlyShowFocusOnTabs()
@ -31,8 +32,8 @@ const App: React.FC<AppProps> = props => {
const [strings, setStrings] = useState<LanguageRecord>() const [strings, setStrings] = useState<LanguageRecord>()
const [token, setToken] = useAPIToken(apiToken) const [token, setToken] = useAPIToken(apiToken)
const getRequestOptions = useCallback((): Partial<RequestInit> => { const getRequestOptions = useCallback((): Partial<RequestInit> => {
return buildResfulReactRequestOptions(token) return buildResfulReactRequestOptions(hooks.useGetToken?.() || apiToken || 'default')
}, [token]) }, []) // eslint-disable-line react-hooks/exhaustive-deps
setBaseRouteInfo(accountId, baseRoutePath) setBaseRouteInfo(accountId, baseRoutePath)
useEffect(() => { useEffect(() => {
@ -48,7 +49,7 @@ const App: React.FC<AppProps> = props => {
return strings ? ( return strings ? (
<StringsContextProvider initialStrings={strings}> <StringsContextProvider initialStrings={strings}>
<AppErrorBoundary> <AppErrorBoundary>
<AppContextProvider value={{ standalone, baseRoutePath, accountId, lang, apiToken, on401, hooks, components }}> <AppContextProvider value={{ standalone, baseRoutePath, accountId, lang, on401, hooks, components }}>
<RestfulProvider <RestfulProvider
base="/" base="/"
requestOptions={getRequestOptions} requestOptions={getRequestOptions}
@ -59,7 +60,7 @@ const App: React.FC<AppProps> = props => {
on401() on401()
} }
}}> }}>
<TooltipContextProvider initialTooltipDictionary={{}}> <TooltipContextProvider initialTooltipDictionary={tooltipDictionary}>
<ModalProvider>{children ? children : <RouteDestinations standalone={standalone} />}</ModalProvider> <ModalProvider>{children ? children : <RouteDestinations standalone={standalone} />}</ModalProvider>
</TooltipContextProvider> </TooltipContextProvider>
</RestfulProvider> </RestfulProvider>

View File

@ -13,7 +13,7 @@ const AppContext = React.createContext<AppContextProps>({
components: {} components: {}
}) })
export const AppContextProvider: React.FC<{ value: AppProps }> = ({ value: initialValue, children }) => { export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(({ value: initialValue, children }) => {
const [appStates, setAppStates] = useState<AppProps>(initialValue) const [appStates, setAppStates] = useState<AppProps>(initialValue)
return ( return (
@ -27,6 +27,6 @@ export const AppContextProvider: React.FC<{ value: AppProps }> = ({ value: initi
{children} {children}
</AppContext.Provider> </AppContext.Provider>
) )
} })
export const useAppContext: () => AppContextProps = () => useContext(AppContext) export const useAppContext: () => AppContextProps = () => useContext(AppContext)

View File

@ -1,5 +1,9 @@
import type React from 'react' import type React from 'react'
import type * as History from 'history'
import type { PermissionOptionsMenuButtonProps } from 'components/Permissions/PermissionsOptionsMenuButton'
import type { OverviewChartsWithToggleProps } from 'components/OverviewChartsWithToggle/OverviewChartsWithToggle'
import type { LangLocale } from './framework/strings/languageLoader' import type { LangLocale } from './framework/strings/languageLoader'
import type { FeatureFlagMap, GitFiltersProps } from './utils/GovernanceUtils'
/** /**
* AppProps defines an interface for host (parent) and * AppProps defines an interface for host (parent) and
@ -7,7 +11,6 @@ import type { LangLocale } from './framework/strings/languageLoader'
* of the child app to be customized from the parent app. * of the child app to be customized from the parent app.
* *
* Areas of customization: * Areas of customization:
*
* - API token * - API token
* - Active user * - Active user
* - Active locale (i18n) * - Active locale (i18n)
@ -59,15 +62,27 @@ export interface AppPathProps {
policyIdentifier?: string policyIdentifier?: string
policySetIdentifier?: string policySetIdentifier?: string
evaluationId?: string evaluationId?: string
pipeline?: string repo?: string
execution?: string branch?: string
} }
/** /**
* AppPropsHook defines a collection of React Hooks that application receives from * AppPropsHook defines a collection of React Hooks that application receives from
* Platform integration. * Platform integration.
*/ */
export interface AppPropsHook {} // eslint-disable-line @typescript-eslint/no-empty-interface export interface AppPropsHook {
usePermission(permissionRequest: any, deps?: Array<any>): Array<boolean>
useGetSchemaYaml(params: any, deps?: Array<any>): Record<string, any>
useFeatureFlags(): FeatureFlagMap
useGetToken(): any
useAppStore(): any
useGitSyncStore(): any
useSaveToGitDialog(props: { onSuccess: any; onClose: any; onProgessOverlayClose: any }): any
useGetListOfBranchesWithStatus(props: any): any
useAnyEnterpriseLicense(): boolean
useCurrentEnterpriseLicense(): boolean
useLicenseStore(): any
} // eslint-disable-line @typescript-eslint/no-empty-interface
/** /**
* AppPropsComponent defines a collection of React Components that application receives from * AppPropsComponent defines a collection of React Components that application receives from
@ -75,4 +90,21 @@ export interface AppPropsHook {} // eslint-disable-line @typescript-eslint/no-e
*/ */
export interface AppPropsComponent { export interface AppPropsComponent {
NGBreadcrumbs: React.FC NGBreadcrumbs: React.FC
RbacButton: React.FC
RbacOptionsMenuButton: React.FC<PermissionOptionsMenuButtonProps>
GitFilters: React.FC<GitFiltersProps>
GitSyncStoreProvider: React.FC
GitContextForm: React.FC<any>
NavigationCheck: React.FC<{
when?: boolean
textProps?: {
contentText?: string
titleText?: string
confirmButtonText?: string
cancelButtonText?: string
}
navigate: (path: string) => void
shouldBlockNavigation?: (location: History.Location) => boolean
}>
OverviewChartsWithToggle: React.FC<OverviewChartsWithToggleProps>
} }

View File

@ -3,37 +3,99 @@ import type { AppPathProps } from 'AppProps'
export enum RoutePath { export enum RoutePath {
SIGNIN = '/signin', SIGNIN = '/signin',
TEST_PAGE1 = '/test-page1', POLICY_DASHBOARD = '/dashboard',
TEST_PAGE2 = '/test-page2', POLICY_LISTING = '/policies',
POLICY_NEW = '/policies/new',
REGISTER = '/register', POLICY_VIEW = '/policies/view/:policyIdentifier',
LOGIN = '/login', //POLICY_EDIT = '/policies/edit/:policyIdentifier',
USERS = '/users', POLICY_EDIT= '/policies/edit/:policyIdentifier/:repo?/:branch?',
ACCOUNT = '/account', POLICY_SETS_LISTING = '/policy-sets',
PIPELINES = '/pipelines', POLICY_SETS_DETAIL = '/policy-sets/:policySetIdentifier',
PIPELINE = '/pipelines/:pipeline', POLICY_EVALUATIONS_LISTING = '/policy-evaluations',
PIPELINE_SETTINGS = '/pipelines/:pipeline/settings', POLICY_EVALUATION_DETAIL = '/policy-evaluations/:evaluationId'
PIPELINE_EXECUTIONS = '/pipelines/:pipeline/executions',
PIPELINE_EXECUTION = '/pipelines/:pipeline/executions/:execution',
PIPELINE_EXECUTION_SETTINGS = '/pipelines/:pipeline/executions/:execution/settings'
} }
export default { export default {
toLogin: (): string => toRouteURL(RoutePath.LOGIN), toSignIn: (): string => toRouteURL(RoutePath.SIGNIN),
toRegister: (): string => toRouteURL(RoutePath.REGISTER), toPolicyDashboard: (): string => toRouteURL(RoutePath.POLICY_DASHBOARD),
toAccount: (): string => toRouteURL(RoutePath.ACCOUNT), toPolicyListing: (): string => toRouteURL(RoutePath.POLICY_LISTING),
toPipelines: (): string => toRouteURL(RoutePath.PIPELINES), toPolicyNew: (): string => toRouteURL(RoutePath.POLICY_NEW),
toPipeline: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string => toPolicyView: ({ policyIdentifier }: Required<Pick<AppPathProps, 'policyIdentifier'>>): string =>
toRouteURL(RoutePath.PIPELINE, { pipeline }), toRouteURL(RoutePath.POLICY_VIEW, { policyIdentifier }),
toPipelineExecutions: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string => toPolicyEdit: ({ policyIdentifier }: Required<Pick<AppPathProps, 'policyIdentifier'>>): string =>
toRouteURL(RoutePath.PIPELINE_EXECUTIONS, { pipeline }), toRouteURL(RoutePath.POLICY_EDIT, { policyIdentifier }),
toPipelineSettings: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string => toPolicySets: (): string => toRouteURL(RoutePath.POLICY_SETS_LISTING),
toRouteURL(RoutePath.PIPELINE_SETTINGS, { pipeline }), toPolicyEvaluations: (): string => toRouteURL(RoutePath.POLICY_EVALUATIONS_LISTING),
toPipelineExecution: ({ pipeline, execution }: AppPathProps): string => toGovernancePolicyDashboard: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.PIPELINE_EXECUTION, { pipeline, execution }), toRouteURL(RoutePath.POLICY_DASHBOARD, {
toPipelineExecutionSettings: ({ pipeline, execution }: AppPathProps): string => orgIdentifier,
toRouteURL(RoutePath.PIPELINE_EXECUTION_SETTINGS, { pipeline, execution }) projectIdentifier,
module
// @see https://github.com/drone/policy-mgmt/blob/main/web/src/RouteDefinitions.ts }),
// for more examples regarding to passing parameters to generate URLs toGovernancePolicyListing: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_LISTING, {
orgIdentifier,
projectIdentifier,
module
}),
toGovernanceNewPolicy: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_NEW, {
orgIdentifier,
projectIdentifier,
module
}),
toGovernanceEditPolicy: ({
orgIdentifier,
projectIdentifier,
policyIdentifier,
module,
repo,
branch
}: RequireField<AppPathProps, 'policyIdentifier'>) =>
toRouteURL(RoutePath.POLICY_EDIT, {
orgIdentifier,
projectIdentifier,
policyIdentifier,
module,
repo,
branch
}),
toGovernanceViewPolicy: ({
orgIdentifier,
projectIdentifier,
policyIdentifier,
module
}: RequireField<AppPathProps, 'policyIdentifier'>) =>
toRouteURL(RoutePath.POLICY_VIEW, {
orgIdentifier,
projectIdentifier,
policyIdentifier,
module
}),
toGovernancePolicySetsListing: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_SETS_LISTING, {
orgIdentifier,
projectIdentifier,
module
}),
toGovernancePolicySetDetail: ({ orgIdentifier, projectIdentifier, policySetIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_SETS_DETAIL, {
orgIdentifier,
projectIdentifier,
module,
policySetIdentifier
}),
toGovernanceEvaluationsListing: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_EVALUATIONS_LISTING, {
orgIdentifier,
projectIdentifier,
module
}),
toGovernanceEvaluationDetail: ({ orgIdentifier, projectIdentifier, evaluationId, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_EVALUATION_DETAIL, {
orgIdentifier,
projectIdentifier,
module,
evaluationId
})
} }

View File

@ -1,75 +1,105 @@
import React from 'react' /* eslint-disable react/display-name */
import { HashRouter, Route, Switch } from 'react-router-dom' import React, { useCallback } from 'react'
import type { AppProps } from 'AppProps' import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
import { SignInPage } from 'pages/signin/SignInPage'
import { NotFoundPage } from 'pages/404/NotFoundPage' import { NotFoundPage } from 'pages/404/NotFoundPage'
import { routePath } from 'RouteUtils' import { routePath, standaloneRoutePath } from './RouteUtils'
import { RoutePath } from 'RouteDefinitions' import { RoutePath } from './RouteDefinitions'
import PolicyControlPage from './pages/PolicyControl/PolicyControlPage'
import Policies from './pages/Policies/Policies'
import PolicyDashboard from './pages/PolicyDashboard/PolicyDashboard'
import PolicySets from './pages/PolicySets/PolicySets'
import PolicyEvaluations from './pages/PolicyEvaluations/PolicyEvaluations'
import { EditPolicy } from './pages/EditPolicy/EditPolicy'
import { ViewPolicy } from './pages/ViewPolicy/ViewPolicy'
import { PolicySetDetail } from './pages/PolicySetDetail/PolicySetDetail'
import { EvaluationDetail } from './pages/EvaluationDetail/EvaluationDetail'
import { Login } from './pages/Login/Login' export const RouteDestinations: React.FC<{ standalone: boolean }> = React.memo(
import { Home } from './pages/Pipelines/Pipelines' ({ standalone }) => {
import { Executions } from './pages/Executions/Executions' // TODO: Add Auth wrapper
import { ExecutionSettings } from './pages/Execution/Settings'
import { PipelineSettings } from './pages/Pipeline/Settings'
import { Account } from './pages/Account/Account'
import { SideNav } from './components/SideNav/SideNav'
export const RouteDestinations: React.FC<Pick<AppProps, 'standalone'>> = ({ standalone }) => { const Destinations: React.FC = useCallback(
// TODO: Add a generic Auth Wrapper () => (
<Switch>
{standalone && (
<Route path={routePath(RoutePath.SIGNIN)}>
<SignInPage />
</Route>
)}
const Destinations: React.FC = () => ( <Route path={routePath(RoutePath.POLICY_DASHBOARD)}>
<Switch> <PolicyControlPage titleKey="overview">
{standalone && ( <PolicyDashboard />
<Route path={routePath(RoutePath.REGISTER)}> </PolicyControlPage>
<Login /> </Route>
</Route>
)}
{standalone && (
<Route path={routePath(RoutePath.LOGIN)}>
<Login />
</Route>
)}
<Route exact path={routePath(RoutePath.PIPELINES)}> <Route path={routePath(RoutePath.POLICY_NEW)}>
<SideNav> <PolicyControlPage titleKey="common.policy.newPolicy">
<Home /> <EditPolicy />
</SideNav> </PolicyControlPage>
</Route> </Route>
<Route exact path={routePath(RoutePath.PIPELINE)}> <Route path={routePath(RoutePath.POLICY_VIEW)}>
<SideNav> <PolicyControlPage titleKey="governance.viewPolicy">
<Executions /> <ViewPolicy />
</SideNav> </PolicyControlPage>
</Route> </Route>
<Route exact path={routePath(RoutePath.PIPELINE_SETTINGS)}> <Route exact path={routePath(RoutePath.POLICY_EDIT)}>
<SideNav> <PolicyControlPage titleKey="governance.editPolicy">
<PipelineSettings /> <EditPolicy />
</SideNav> </PolicyControlPage>
</Route> </Route>
<Route exact path={routePath(RoutePath.PIPELINE_EXECUTION_SETTINGS)}> <Route path={routePath(RoutePath.POLICY_LISTING)}>
<SideNav> <PolicyControlPage titleKey="common.policies">
<ExecutionSettings /> <Policies />
</SideNav> </PolicyControlPage>
</Route> </Route>
<Route exact path={routePath(RoutePath.ACCOUNT)}> <Route exact path={routePath(RoutePath.POLICY_SETS_LISTING)}>
<SideNav> <PolicyControlPage titleKey="common.policy.policysets">
<Account /> <PolicySets />
</SideNav> </PolicyControlPage>
</Route> </Route>
<Route path="/"> <Route path={routePath(RoutePath.POLICY_SETS_DETAIL)}>
<NotFoundPage /> <PolicyControlPage titleKey="common.policy.policysets">
</Route> <PolicySetDetail />
</Switch> </PolicyControlPage>
) </Route>
return standalone ? ( <Route path={routePath(RoutePath.POLICY_EVALUATION_DETAIL)}>
<HashRouter> <PolicyControlPage titleKey="governance.evaluations">
<EvaluationDetail />
</PolicyControlPage>
</Route>
<Route path={routePath(RoutePath.POLICY_EVALUATIONS_LISTING)}>
<PolicyControlPage titleKey="governance.evaluations">
<PolicyEvaluations />
</PolicyControlPage>
</Route>
<Route path="/">
{standalone ? (
<Redirect to={standaloneRoutePath(RoutePath.POLICY_DASHBOARD)} />
) : (
<NotFoundPage />
)}
</Route>
</Switch>
),
[standalone]
)
return standalone ? (
<HashRouter>
<Destinations />
</HashRouter>
) : (
<Destinations /> <Destinations />
</HashRouter> )
) : ( }
<Destinations /> )
)
}

View File

@ -14,40 +14,42 @@ type Scope = Pick<AppPathProps, 'orgIdentifier' | 'projectIdentifier' | 'module'
// //
// Note: This function needs to be in sync with NextGen UI's routeUtils' getScopeBasedRoute. When // Note: This function needs to be in sync with NextGen UI's routeUtils' getScopeBasedRoute. When
// it's out of sync, the URL routing scheme could be broken. // it's out of sync, the URL routing scheme could be broken.
// @see https://github.com/harness/harness-core-ui/blob/master/src/modules/10-common/utils/routeUtils.ts#L171 // @see https://github.com/wings-software/nextgenui/blob/master/src/modules/10-common/utils/routeUtils.ts#L171
// //
const getScopeBasedRouteURL = ({ path, scope = {} }: { path: string; scope?: Scope }): string => { const getScopeBasedRouteURL = ({ path, scope = {} }: { path: string; scope?: Scope }): string => {
if (window.APP_RUN_IN_STANDALONE_MODE) {
return path
}
const { orgIdentifier, projectIdentifier, module } = scope const { orgIdentifier, projectIdentifier, module } = scope
// // The Governance app is mounted in three places in Harness Platform
// TODO: Change this scheme below to reflect your application when it's embedded into Harness NextGen UI // 1. Account Settings (account level governance)
// // 2. Org Details (org level governance)
// 3. Project Settings (project level governance)
// The Sample Module UI app is mounted in three places in Harness Platform
// 1. Account Settings (account level)
// 2. Org Details (org level)
// 3. Project Settings (project level)
if (module && orgIdentifier && projectIdentifier) { if (module && orgIdentifier && projectIdentifier) {
return `/account/${accountId}/${module}/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/sample-module${path}` return `/account/${accountId}/${module}/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/governance${path}`
} else if (orgIdentifier && projectIdentifier) { } else if (orgIdentifier && projectIdentifier) {
return `/account/${accountId}/home/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/sample-module${path}` return `/account/${accountId}/home/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/governance${path}`
} else if (orgIdentifier) { } else if (orgIdentifier) {
return `/account/${accountId}/settings/organizations/${orgIdentifier}/setup/sample-module${path}` return `/account/${accountId}/settings/organizations/${orgIdentifier}/setup/governance${path}`
} }
return `/account/${accountId}/settings/sample-module${path}` return `/account/${accountId}/settings/governance${path}`
} }
/** /**
* Generate route path to be used in RouteDefinitions. * Generate route paths to be used in RouteDefinitions.
* @param path route path * @param path route path
* @returns a proper route path that works in both standalone and embedded modes. * @returns an array of proper route paths that works in both standalone and embedded modes across all levels of governance.
*/ */
export const routePath = (path: string): string => `${baseRoutePath || ''}${path}` export const routePath = (path: string): string[] => [
`/account/:accountId/settings/governance${path}`,
`/account/:accountId/settings/organizations/:orgIdentifier/setup/governance${path}`,
`/account/:accountId/:module(cd)/orgs/:orgIdentifier/projects/:projectIdentifier/setup/governance${path}`,
`/account/:accountId/:module(ci)/orgs/:orgIdentifier/projects/:projectIdentifier/setup/governance${path}`,
`/account/:accountId/:module(cf)/orgs/:orgIdentifier/projects/:projectIdentifier/setup/governance${path}`,
`/account/:accountId/:module(sto)/orgs/:orgIdentifier/projects/:projectIdentifier/setup/governance${path}`,
`/account/:accountId/:module(cv)/orgs/:orgIdentifier/projects/:projectIdentifier/setup/governance${path}`,
]
export const standaloneRoutePath = (path: string): string => `${baseRoutePath || ''}${path}`
/** /**
* Generate route URL to be used RouteDefinitions' default export (aka actual react-router link href) * Generate route URL to be used RouteDefinitions' default export (aka actual react-router link href)

View File

@ -1,10 +1,21 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import App from './App' import App from './App'
import './App.scss'
// This flag is used in services/config.ts to customize API path when app is run // This flag is used in services/config.ts to customize API path when app is run
// in multiple modes (standalone vs. embedded). // in multiple modes (standalone vs. embedded).
// Also being used in when generating proper URLs inside the app. // Also being used in when generating proper URLs inside the app.
window.APP_RUN_IN_STANDALONE_MODE = true window.STRIP_PM_PREFIX = true
ReactDOM.render(<App standalone hooks={{}} components={{}} />, document.getElementById('react-root')) ReactDOM.render(
<App
standalone
accountId="default"
apiToken="default"
baseRoutePath="/account/default/settings/governance"
hooks={{}}
components={{}}
/>,
document.getElementById('react-root')
)

View File

@ -1,8 +0,0 @@
.spinner {
width: 100%;
height: 100%;
> div {
position: relative !important;
}
}

View File

@ -1,12 +0,0 @@
import React from 'react'
import cx from 'classnames'
import { Container, PageSpinner } from '@harness/uicore'
import css from './ContainerSpinner.module.scss'
export const ContainerSpinner: React.FC<React.ComponentProps<typeof Container>> = ({ className, ...props }) => {
return (
<Container className={cx(css.spinner, className)} {...props}>
<PageSpinner />
</Container>
)
}

View File

@ -1,42 +0,0 @@
.status {
--bg-color: var(--grey-350);
white-space: nowrap !important;
font-size: var(--font-size-xsmall) !important;
color: var(--white) !important;
border: none;
background-color: var(--bg-color) !important;
border-radius: var(--spacing-2);
padding: var(--spacing-1) var(--spacing-3) !important;
height: 18px;
line-height: var(--font-size-normal) !important;
font-weight: bold !important;
display: inline-flex !important;
justify-content: center;
align-items: center;
letter-spacing: 0.2px;
&.danger {
--bg-color: var(--red-600);
}
&.none {
--bg-color: var(--grey-800);
}
&.success {
--bg-color: var(--green-600);
}
&.primary {
--bg-color: var(--primary-7);
}
&.warning {
--bg-color: var(--warning);
}
> span {
margin-right: var(--spacing-2) !important;
color: var(--white) !important;
}
}

View File

@ -1,11 +0,0 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly status: string
readonly danger: string
readonly none: string
readonly success: string
readonly primary: string
readonly warning: string
}
export default styles

View File

@ -1,41 +0,0 @@
import React from 'react'
import cx from 'classnames'
import { Intent, IconName, Text } from '@harness/uicore'
import type { IconProps } from '@harness/uicore/dist/icons/Icon'
import css from './EvaluationStatusLabel.module.scss'
export interface EvaluationStatusProps {
intent: Intent
label: string
icon?: IconName
iconProps?: IconProps
className?: string
}
export const EvaluationStatusLabel: React.FC<EvaluationStatusProps> = ({
intent,
icon,
iconProps,
label,
className
}) => {
let _icon: IconName | undefined = icon
if (!_icon) {
switch (intent) {
case Intent.DANGER:
case Intent.WARNING:
_icon = 'warning-sign'
break
case Intent.SUCCESS:
_icon = 'tick-circle'
break
}
}
return (
<Text icon={_icon} iconProps={{ size: 9, ...iconProps }} className={cx(css.status, className, css[intent])}>
{label}
</Text>
)
}

View File

@ -38,7 +38,7 @@ interface NameIdProps {
export const NameId = (props: NameIdProps): JSX.Element => { export const NameId = (props: NameIdProps): JSX.Element => {
const { getString } = useStrings() const { getString } = useStrings()
const { identifierProps, nameLabel = getString('common.name'), inputGroupProps = {} } = props const { identifierProps, nameLabel = getString('name'), inputGroupProps = {} } = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps } const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
return ( return (
<FormInput.InputWithIdentifier inputLabel={nameLabel} inputGroupProps={newInputGroupProps} {...identifierProps} /> <FormInput.InputWithIdentifier inputLabel={nameLabel} inputGroupProps={newInputGroupProps} {...identifierProps} />
@ -55,9 +55,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => {
return ( return (
<Container style={{ marginBottom: isDescriptionOpen ? '0' : 'var(--spacing-medium)' }}> <Container style={{ marginBottom: isDescriptionOpen ? '0' : 'var(--spacing-medium)' }}>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}> <Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional {isOptional ? getString('optionalField', { name: getString('description') }) : getString('description')}
? getString('common.optionalField', { name: getString('common.description') })
: getString('common.description')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null} {props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isDescriptionOpen && ( {!isDescriptionOpen && (
<Icon <Icon
@ -79,7 +77,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => {
disabled={disabled} disabled={disabled}
autoFocus={isDescriptionFocus} autoFocus={isDescriptionFocus}
name="description" name="description"
placeholder={getString('common.descriptionPlaceholder')} placeholder={getString('descriptionPlaceholder')}
{...restDescriptionProps} {...restDescriptionProps}
/> />
)} )}
@ -95,9 +93,7 @@ export const Tags = (props: TagsComponentProps): JSX.Element => {
return ( return (
<Container> <Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}> <Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional {isOptional ? getString('optionalField', { name: getString('tagsLabel') }) : getString('tagsLabel')}
? getString('common.optionalField', { name: getString('common.tagsLabel') })
: getString('common.tagsLabel')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null} {props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isTagsOpen && ( {!isTagsOpen && (
<Icon <Icon
@ -125,7 +121,7 @@ function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
return ( return (
<Container> <Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)}> <Label className={cx(Classes.LABEL, css.descriptionLabel)}>
{getString('common.tagsLabel')} {getString('tagsLabel')}
{!isTagsOpen && ( {!isTagsOpen && (
<Icon <Icon
className={css.editOpen} className={css.editOpen}
@ -159,15 +155,7 @@ function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element { export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element {
const { getString } = useStrings() const { getString } = useStrings()
const { const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {}, tooltipProps } = props
className,
identifierProps,
descriptionProps,
tagsProps,
formikProps,
inputGroupProps = {},
tooltipProps
} = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps } const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
return ( return (
<Container className={cx(css.main, className)}> <Container className={cx(css.main, className)}>
@ -177,12 +165,6 @@ export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.El
hasValue={!!formikProps?.values.description} hasValue={!!formikProps?.values.description}
dataTooltipId={tooltipProps?.dataTooltipId ? `${tooltipProps.dataTooltipId}_description` : undefined} dataTooltipId={tooltipProps?.dataTooltipId ? `${tooltipProps.dataTooltipId}_description` : undefined}
/> />
<Tags
tagsProps={tagsProps}
isOptional={tagsProps?.isOption}
hasValue={!isEmpty(formikProps?.values.tags)}
dataTooltipId={tooltipProps?.dataTooltipId ? `${tooltipProps.dataTooltipId}_tags` : undefined}
/>
</Container> </Container>
) )
} }

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { ReactElement } from 'react'
import { Classes, Menu } from '@blueprintjs/core' import { Classes, Menu } from '@blueprintjs/core'
import { Button, ButtonProps } from '@harness/uicore' import { Button, ButtonProps } from '@harness/uicore'
import type { PopoverProps } from '@harness/uicore/dist/components/Popover/Popover' import type { PopoverProps } from '@harness/uicore/dist/components/Popover/Popover'
@ -9,7 +9,7 @@ export interface OptionsMenuButtonProps extends ButtonProps {
items: Array<React.ComponentProps<typeof Menu.Item> | '-'> items: Array<React.ComponentProps<typeof Menu.Item> | '-'>
} }
export const OptionsMenuButton: React.FC<OptionsMenuButtonProps> = ({ items, ...props }) => { export const OptionsMenuButton = ({ items, ...props }: OptionsMenuButtonProps): ReactElement => {
return ( return (
<Button <Button
minimal minimal

View File

@ -0,0 +1,16 @@
import React from 'react'
import { Button, ButtonProps } from '@harness/uicore'
import { useAppContext } from 'AppContext'
interface PermissionButtonProps extends ButtonProps {
permission?: any
}
export const PermissionsButton: React.FC<PermissionButtonProps> = (props: PermissionButtonProps) => {
const {
components: { RbacButton }
} = useAppContext()
const { permission, ...buttonProps } = props
return RbacButton ? <RbacButton permission={permission} {...props} /> : <Button {...buttonProps} />
}

View File

@ -0,0 +1,22 @@
import React, { AnchorHTMLAttributes, ReactElement } from 'react'
import type { IMenuItemProps } from '@blueprintjs/core'
import { OptionsMenuButton, OptionsMenuButtonProps } from 'components/OptionsMenuButton/OptionsMenuButton'
import { useAppContext } from 'AppContext'
type Item = ((IMenuItemProps | PermissionsMenuItemProps) & AnchorHTMLAttributes<HTMLAnchorElement>) | '-'
interface PermissionsMenuItemProps extends IMenuItemProps {
permission?: any
}
export interface PermissionOptionsMenuButtonProps extends OptionsMenuButtonProps {
items: Item[]
}
export const PermissionsOptionsMenuButton = (props: PermissionOptionsMenuButtonProps): ReactElement => {
const {
components: { RbacOptionsMenuButton }
} = useAppContext()
return RbacOptionsMenuButton ? <RbacOptionsMenuButton {...props} /> : <OptionsMenuButton {...props} />
}

View File

@ -1,24 +0,0 @@
.root {
display: flex;
flex-direction: column;
flex: 1 auto;
}
.container {
box-shadow: var(--card-shadow);
}
.minWidth {
min-width: 400px;
}
.input {
margin-bottom: unset !important;
}
.pre {
background: var(--theme-dark-canvas-dot);
color: #fff;
width: 300px;
height: auto;
}

View File

@ -1,16 +0,0 @@
/* eslint-disable */
/**
* Copyright 2021 Harness Inc. All rights reserved.
* Use of this source code is governed by the PolyForm Shield 1.0.0 license
* that can be found in the licenses directory at the root of this repository, also available at
* https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
**/
// this is an auto-generated file, do not update this manually
declare const styles: {
readonly container: string
readonly input: string
readonly minWidth: string
readonly pre: string
readonly root: string
}
export default styles

View File

@ -1,126 +0,0 @@
import React, { useState } from 'react'
import {
Container,
Button,
Formik,
FormikForm,
FormInput,
Text,
Color,
Layout,
ButtonVariation,
Page,
CodeBlock
} from '@harness/uicore'
import { useAPIToken } from 'hooks/useAPIToken'
import { useStrings } from 'framework/strings'
import styles from './Settings.module.scss'
interface FormValues {
name?: string
desc?: string
}
interface FormProps {
name?: string
desc?: string
handleSubmit: (values: FormValues) => void
loading: boolean | undefined
refetch: () => void
handleDelete: () => void
error?: any
title: string
}
export const Settings = ({ name, desc, handleSubmit, handleDelete, loading, refetch, error, title }: FormProps) => {
const [token] = useAPIToken()
const { getString } = useStrings()
const [showToken, setShowToken] = useState(false)
const [editDetails, setEditDetails] = useState(false)
const onSubmit = (values: FormValues) => {
handleSubmit(values)
setEditDetails(false)
}
const editForm = (
<Formik initialValues={{ name, desc }} formName="newPipelineForm" onSubmit={values => onSubmit(values)}>
<FormikForm>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} margin={{ bottom: 'large' }}>
<Text color={Color.GREY_600} className={styles.minWidth}>
{getString('common.name')}
</Text>
<FormInput.Text name="name" className={styles.input} />
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} margin={{ bottom: 'large' }}>
<Text color={Color.GREY_600} className={styles.minWidth}>
{getString('common.description')}
</Text>
<FormInput.Text name="desc" className={styles.input} />
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} margin={{ bottom: 'large' }}>
<Button variation={ButtonVariation.LINK} icon="updated" text={getString('common.save')} type="submit" />
<Button variation={ButtonVariation.LINK} onClick={handleDelete}>
Delete
</Button>
</Layout.Horizontal>
</FormikForm>
</Formik>
)
return (
<Container className={styles.root} height="inherit">
<Page.Header title={getString('settings')} />
<Page.Body
loading={loading}
retryOnError={() => refetch()}
error={(error?.data as Error)?.message || error?.message}>
<Container margin="xlarge" padding="xlarge" className={styles.container} background="white">
<Text color={Color.BLACK} font={{ weight: 'semi-bold', size: 'medium' }} margin={{ bottom: 'xlarge' }}>
{title}
</Text>
{editDetails ? (
editForm
) : (
<>
<Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
margin={{ bottom: 'large' }}>
<Text color={Color.GREY_600} className={styles.minWidth}>
{getString('common.name')}
</Text>
<Text color={Color.GREY_800}>{name}</Text>
</Layout.Horizontal>
<Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
margin={{ bottom: 'large' }}>
<Text className={styles.minWidth}>{getString('common.description')}</Text>
<Text color={Color.GREY_800}>{desc}</Text>
</Layout.Horizontal>
</>
)}
{!editDetails && (
<Button
variation={ButtonVariation.LINK}
icon="Edit"
text={getString('common.edit')}
onClick={() => setEditDetails(true)}
/>
)}
</Container>
<Container margin="xlarge" padding="xlarge" className={styles.container} background="white">
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} margin={{ bottom: 'large' }}>
<Text className={styles.minWidth}>{getString('common.token')}</Text>
<Button variation={ButtonVariation.LINK} onClick={() => setShowToken(!showToken)}>
Display/Hide Token
</Button>
</Layout.Horizontal>
<Layout.Horizontal flex={{ alignItems: 'center', justifyContent: 'flex-start' }} margin={{ bottom: 'large' }}>
{showToken && <CodeBlock allowCopy format="pre" snippet={token} />}
</Layout.Horizontal>
</Container>
</Page.Body>
</Container>
)
}

View File

@ -1,40 +0,0 @@
.root {
display: flex;
}
.sideNav {
width: 184px !important;
height: 100%;
display: flex;
flex-direction: column;
position: relative;
background: #07182b !important;
}
.link {
display: block;
margin-left: var(--spacing-medium);
padding: var(--spacing-small) var(--spacing-medium);
opacity: 0.8;
z-index: 1;
&:hover {
text-decoration: none;
opacity: 1;
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
background-color: rgba(2, 120, 213, 0.5);
}
&.selected {
background-color: rgba(2, 120, 213, 0.8);
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
opacity: 1;
}
.text {
color: var(--white) !important;
font-size: 13px !important;
}
}

View File

@ -1,16 +0,0 @@
/* eslint-disable */
/**
* Copyright 2021 Harness Inc. All rights reserved.
* Use of this source code is governed by the PolyForm Shield 1.0.0 license
* that can be found in the licenses directory at the root of this repository, also available at
* https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
**/
// this is an auto-generated file, do not update this manually
declare const styles: {
readonly link: string
readonly root: string
readonly selected: string
readonly sideNav: string
readonly text: string
}
export default styles

View File

@ -1,38 +0,0 @@
import React from 'react'
import cx from 'classnames'
import { NavLink as Link, NavLinkProps } from 'react-router-dom'
import { Container, Text, Layout, IconName } from '@harness/uicore'
import { useAPIToken } from 'hooks/useAPIToken'
import { useStrings } from 'framework/strings'
import routes from 'RouteDefinitions'
import css from './SideNav.module.scss'
interface SidebarLinkProps extends NavLinkProps {
label: string
icon?: IconName
className?: string
}
const SidebarLink: React.FC<SidebarLinkProps> = ({ label, icon, className, ...others }) => (
<Link className={cx(css.link, className)} activeClassName={css.selected} {...others}>
<Text icon={icon} className={css.text}>
{label}
</Text>
</Link>
)
export const SideNav: React.FC = ({ children }) => {
const { getString } = useStrings()
const [, setToken] = useAPIToken()
return (
<Container height="inherit" className={css.root}>
<Layout.Vertical spacing="small" padding={{ top: 'xxxlarge' }} className={css.sideNav}>
<SidebarLink exact icon="pipeline" label={getString('pipelines')} to={routes.toPipelines()} />
<SidebarLink exact icon="advanced" label={getString('account')} to={routes.toAccount()} />
<SidebarLink onClick={() => setToken('')} icon="log-out" label={getString('logout')} to={routes.toLogin()} />
</Layout.Vertical>
{children}
</Container>
)
}

View File

@ -0,0 +1,11 @@
.loadingSpinnerWrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.hidden {
display: none !important;
width: 0px
}

View File

@ -1,6 +1,7 @@
/* eslint-disable */ /* eslint-disable */
// this is an auto-generated file // this is an auto-generated file
declare const styles: { declare const styles: {
readonly spinner: string readonly loadingSpinnerWrapper: string
readonly hidden: string
} }
export default styles export default styles

View File

@ -0,0 +1,24 @@
import React, { CSSProperties } from 'react'
import { Layout } from '@harness/uicore'
import { Spinner } from '@blueprintjs/core'
import cx from 'classnames'
import css from './SpinnerWrapper.module.scss'
export const SpinnerWrapper = ({
loading,
children,
style
}: {
loading: boolean
children: React.ReactNode | undefined
style?: CSSProperties
}): JSX.Element => {
return (
<Layout.Vertical style={style}>
<Layout.Horizontal className={cx(css.loadingSpinnerWrapper, { [css.hidden]: !loading })}>
<Spinner />
</Layout.Horizontal>
{!loading && children}
</Layout.Vertical>
)
}

View File

@ -1,11 +0,0 @@
.table {
padding-bottom: 0;
}
.layout {
justify-content: flex-end;
}
.verticalCenter {
justify-content: center;
}

View File

@ -1,14 +0,0 @@
/* eslint-disable */
/**
* Copyright 2021 Harness Inc. All rights reserved.
* Use of this source code is governed by the PolyForm Shield 1.0.0 license
* that can be found in the licenses directory at the root of this repository, also available at
* https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt.
**/
// this is an auto-generated file, do not update this manually
declare const styles: {
readonly layout: string
readonly table: string
readonly verticalCenter: string
}
export default styles

View File

@ -1,172 +0,0 @@
import React, { useMemo, useState } from 'react'
import moment from 'moment'
import {
Text,
Layout,
Color,
TableV2,
Button,
ButtonVariation,
useConfirmationDialog,
useToaster
} from '@harness/uicore'
import type { CellProps, Renderer, Column } from 'react-table'
import { Menu, Position, Intent, Popover } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import type { Pipeline } from 'services/pm'
import styles from './Table.module.scss'
interface TableProps {
data: Pipeline[] | null
refetch: () => Promise<void>
onDelete: (value: string) => Promise<void>
onSettingsClick: (slug: string) => void
onRowClick: (slug: string) => void
}
type CustomColumn<T extends Record<string, any>> = Column<T> & {
refetch?: () => Promise<void>
}
const Table: React.FC<TableProps> = ({ data, refetch, onRowClick, onDelete, onSettingsClick }) => {
const RenderColumn: Renderer<CellProps<Pipeline>> = ({
cell: {
column: { Header },
row: { values }
}
}) => {
let text
switch (Header) {
case 'ID':
text = values.id
break
case 'Name':
text = values.name
break
case 'Description':
text = values.desc
break
case 'Slug':
text = values.slug
break
case 'Created':
text = moment(values.created).format('MM/DD/YYYY hh:mm:ss a')
break
}
return (
<Layout.Horizontal
onClick={() => onRowClick(values.slug)}
spacing="small"
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
style={{ cursor: 'pointer' }}>
<Layout.Vertical spacing="xsmall" padding={{ left: 'small' }} className={styles.verticalCenter}>
<Layout.Horizontal spacing="small">
<Text color={Color.BLACK} lineClamp={1}>
{text}
</Text>
</Layout.Horizontal>
</Layout.Vertical>
</Layout.Horizontal>
)
}
const RenderColumnMenu: Renderer<CellProps<Pipeline>> = ({ row: { values } }) => {
const { showSuccess, showError } = useToaster()
const { getString } = useStrings()
const [menuOpen, setMenuOpen] = useState(false)
const { openDialog } = useConfirmationDialog({
titleText: getString('common.delete'),
contentText: <Text color={Color.GREY_800}>Are you sure you want to delete this?</Text>,
confirmButtonText: getString('common.delete'),
cancelButtonText: getString('common.cancel'),
intent: Intent.DANGER,
buttonIntent: Intent.DANGER,
onCloseDialog: async (isConfirmed: boolean) => {
if (isConfirmed) {
try {
await onDelete(values.slug)
showSuccess(getString('common.itemDeleted'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error({ err })
}
}
}
})
return (
<Layout.Horizontal className={styles.layout}>
<Popover
isOpen={menuOpen}
onInteraction={nextOpenState => setMenuOpen(nextOpenState)}
position={Position.BOTTOM_RIGHT}
content={
<Menu style={{ minWidth: 'unset' }}>
<Menu.Item icon="trash" text={getString('common.delete')} onClick={openDialog} />
<Menu.Item icon="settings" text={getString('settings')} onClick={() => onSettingsClick(values.slug)} />
</Menu>
}>
<Button icon="Options" variation={ButtonVariation.ICON} />
</Popover>
</Layout.Horizontal>
)
}
const columns: CustomColumn<Pipeline>[] = useMemo(
() => [
{
Header: 'ID',
id: 'id',
accessor: row => row.id,
width: '15%',
Cell: RenderColumn
},
{
Header: 'Name',
id: 'name',
accessor: row => row.name,
width: '20%',
Cell: RenderColumn
},
{
Header: 'Description',
id: 'desc',
accessor: row => row.desc,
width: '30%',
Cell: RenderColumn,
disableSortBy: true
},
{
Header: 'Slug',
id: 'slug',
accessor: row => row.slug,
width: '15%',
Cell: RenderColumn,
disableSortBy: true
},
{
Header: 'Created',
id: 'created',
accessor: row => row.created,
width: '15%',
Cell: RenderColumn,
disableSortBy: true
},
{
Header: '',
id: 'menu',
accessor: row => row.slug,
width: '5%',
Cell: RenderColumnMenu,
disableSortBy: true,
refetch: refetch
}
],
[refetch]
)
return <TableV2<Pipeline> className={styles.table} columns={columns} name="basicTable" data={data || []} />
}
export default Table

View File

@ -0,0 +1,15 @@
.banner {
box-shadow: 0px 0px 1px rgba(40, 41, 61, 0.08), 0px 0.5px 2px rgba(96, 97, 112, 0.16);
&.expiryCountdown {
background: var(--orange-50) !important;
}
&.expired {
background: var(--red-50) !important;
}
.bannerIcon {
margin-right: var(--spacing-large) !important;
}
}

View File

@ -0,0 +1,9 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly banner: string
readonly expiryCountdown: string
readonly expired: string
readonly bannerIcon: string
}
export default styles

View File

@ -0,0 +1,53 @@
import React, { ReactElement } from 'react'
import cx from 'classnames'
import moment from 'moment'
import { Container, Icon, Text } from '@harness/uicore'
import { Color } from '@harness/design-system'
import { useGetTrialInfo } from 'utils/GovernanceUtils'
import { useStrings } from 'framework/strings'
import css from './TrialBanner.module.scss'
const TrialBanner = (): ReactElement => {
const trialInfo = useGetTrialInfo()
const { getString } = useStrings()
if (!trialInfo) return <></>
const { expiryTime } = trialInfo
const time = moment(trialInfo.expiryTime)
const days = Math.round(time.diff(moment.now(), 'days', true))
const expiryDate = time.format('DD MMM YYYY')
const isExpired = expiryTime !== -1 && days < 0
const expiredDays = Math.abs(days)
const expiryMessage = isExpired
? getString('banner.expired', {
days: expiredDays
})
: getString('banner.expiryCountdown', {
days
})
const bannerMessage = `Harness Policy Engine trial ${expiryMessage} on ${expiryDate}`
const bannerClassnames = cx(css.banner, isExpired ? css.expired : css.expiryCountdown)
const color = isExpired ? Color.RED_700 : Color.ORANGE_700
return (
<Container
padding="small"
intent="warning"
flex={{
justifyContent: 'start'
}}
className={bannerClassnames}
font={{
align: 'center'
}}>
<Icon name={'warning-sign'} size={15} className={css.bannerIcon} color={color} />
<Text color={color}>{bannerMessage}</Text>
</Container>
)
}
export default TrialBanner

View File

@ -3,37 +3,143 @@
* Use the command `yarn strings` to regenerate this file. * Use the command `yarn strings` to regenerate this file.
*/ */
export interface StringsMap { export interface StringsMap {
account: string AZ09: string
addExecution: string ZA90: string
'common.accountDetails': string action: string
'common.accountOverview': string all: string
'common.cancel': string apply: string
'common.delete': string back: string
'common.deleteConfirm': string 'banner.expired': string
'common.description': string 'banner.expiryCountdown': string
'common.descriptionPlaceholder': string cancel: string
'common.edit': string clearFilter: string
'common.email': string
'common.itemCreated': string
'common.itemDeleted': string
'common.itemUpdated': string
'common.name': string
'common.namePlaceholder': string 'common.namePlaceholder': string
'common.optionalField': string 'common.policies': string
'common.save': string 'common.policiesSets.created': string
'common.tagsLabel': string 'common.policiesSets.enforced': string
'common.token': string 'common.policiesSets.entity': string
created: string 'common.policiesSets.evaluationCriteria': string
executions: string 'common.policiesSets.event': string
existingAccount: string 'common.policiesSets.newPolicyset': string
logout: string 'common.policiesSets.noPolicySet': string
noAccount: string 'common.policiesSets.noPolicySetDescription': string
pageNotFound: string 'common.policiesSets.noPolicySetResult': string
password: string 'common.policiesSets.noPolicySetTitle': string
pipelineSettings: string 'common.policiesSets.noPolicySets': string
pipelines: string 'common.policiesSets.policySetSearch': string
settings: string 'common.policiesSets.scope': string
signUp: string 'common.policiesSets.stepOne.validId': string
signin: string 'common.policiesSets.stepOne.validIdRegex': string
slug: string 'common.policiesSets.stepOne.validName': string
'common.policiesSets.table.enforced': string
'common.policiesSets.table.entityType': string
'common.policiesSets.table.name': string
'common.policiesSets.updated': string
'common.policy.evaluations': string
'common.policy.newPolicy': string
'common.policy.noPolicy': string
'common.policy.noPolicyEvalResult': string
'common.policy.noPolicyEvalResultTitle': string
'common.policy.noPolicyResult': string
'common.policy.noPolicyTitle': string
'common.policy.noSelectInput': string
'common.policy.permission.noEdit': string
'common.policy.policySearch': string
'common.policy.policysets': string
'common.policy.table.createdAt': string
'common.policy.table.lastModified': string
'common.policy.table.name': string
confirm: string
continue: string
delete: string
description: string
descriptionPlaceholder: string
details: string
edit: string
entity: string
'evaluation.evaluatedPoliciesCount': string
'evaluation.onePolicyEvaluated': string
executionsText: string
failed: string
fileOverwrite: string
finish: string
'governance.clearOutput': string
'governance.deleteConfirmation': string
'governance.deleteDone': string
'governance.deletePolicySetConfirmation': string
'governance.deletePolicySetDone': string
'governance.deletePolicySetTitle': string
'governance.deleteTitle': string
'governance.editPolicy': string
'governance.editPolicyMetadataTitle': string
'governance.emptyPolicySet': string
'governance.evaluatedOn': string
'governance.evaluatedTime': string
'governance.evaluationEmpty': string
'governance.evaluations': string
'governance.event': string
'governance.failureHeading': string
'governance.failureHeadingEvaluationDetail': string
'governance.failureModalTitle': string
'governance.formatInput': string
'governance.inputFailedEvaluation': string
'governance.inputSuccededEvaluation': string
'governance.noEvaluationForPipeline': string
'governance.noPolicySetForPipeline': string
'governance.onCreate': string
'governance.onRun': string
'governance.onSave': string
'governance.onStep': string
'governance.policyAccountCount': string
'governance.policyDescription': string
'governance.policyIdentifier': string
'governance.policyName': string
'governance.policyOrgCount': string
'governance.policyProjectCount': string
'governance.policySetGroup': string
'governance.policySetGroupAccount': string
'governance.policySetGroupOrg': string
'governance.policySetGroupProject': string
'governance.policySetName': string
'governance.policySets': string
'governance.policySetsApplied': string
'governance.selectInput': string
'governance.selectSamplePolicy': string
'governance.successHeading': string
'governance.viewPolicy': string
'governance.warn': string
'governance.warning': string
'governance.warningHeading': string
'governance.warningHeadingEvaluationDetail': string
'governance.wizard.fieldArray': string
'governance.wizard.policySelector.account': string
'governance.wizard.policySelector.org': string
'governance.wizard.policySelector.selectPolicy': string
'governance.wizard.policyToEval': string
input: string
lastUpdated: string
name: string
navigationCheckText: string
navigationCheckTitle: string
no: string
noSearchResultsFound: string
optionalField: string
outputLabel: string
overview: string
samplePolicies: string
saveOverwrite: string
search: string
source: string
status: string
success: string
tagsLabel: string
type: string
useSample: string
'validation.identifierIsRequired': string
'validation.identifierRequired': string
'validation.nameRequired': string
'validation.policySaveButtonMessage': string
'validation.thisIsARequiredField': string
'validation.validIdRegex': string
yes: string
} }

4
web/src/global.d.ts vendored
View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
declare const __DEV__: boolean declare const __DEV__: boolean
declare const __ON_PREM__: boolean declare const __ON_PREM__: booelan
declare module '*.png' { declare module '*.png' {
const value: string const value: string
@ -45,7 +45,7 @@ declare module '*.gql' {
declare interface Window { declare interface Window {
apiUrl: string apiUrl: string
bugsnagClient?: any bugsnagClient?: any
APP_RUN_IN_STANDALONE_MODE?: boolean STRIP_PM_PREFIX?: boolean
} }
declare const monaco: any declare const monaco: any

View File

@ -0,0 +1,11 @@
import type { FeatureFlagMap } from '../utils/GovernanceUtils'
export function useStandaloneFeatureFlags(): FeatureFlagMap {
return {
OPA_PIPELINE_GOVERNANCE: true,
OPA_FF_GOVERNANCE: false,
CUSTOM_POLICY_STEP: false,
OPA_GIT_GOVERNANCE: false,
OPA_SECRET_GOVERNANCE: false
}
}

View File

@ -0,0 +1,3 @@
export function useStandalonePermission(_permissionsRequest?: any, _deps: Array<any> = []): Array<boolean> {
return [true, true]
}

View File

@ -1,34 +1,152 @@
signin: Sign In failed: Failed
signUp: Sign Up status: Status
logout: Logout success: Success
password: Password details: Details
pageNotFound: Page Not Found overview: Overview
back: Back
finish: Finish
delete: Delete
apply: Apply
cancel: Cancel
continue: Continue
type: Type
name: Name
action: Action
edit: Edit
executionsText: Executions
outputLabel: Output
description: Description
optionalField: '{{name}} (optional)'
descriptionPlaceholder: Enter Description
tagsLabel: Tags
yes: Yes
no: No
source: Source
common: common:
save: Save
edit: Edit
name: Name
email: Email
namePlaceholder: Enter Name namePlaceholder: Enter Name
description: Description policies: 'Policies'
descriptionPlaceholder: Enter Description policy:
tagsLabel: Tags policysets: Policy Sets
optionalField: '{{name}} (optional)' newPolicy: New Policy
delete: Delete evaluations: Evaluations
deleteConfirm: Are you sure you want to delete this? policySearch: Search Policy by name
itemDeleted: Item Deleted noPolicy: A Harness policy is an OPA rule that can be enforced on your Harness software delivery processes to ensure governance and compliance.
itemUpdated: Item Updated noPolicyTitle: You have no policies
itemCreated: Item Created noPolicyResult: No policies found
cancel: Cancel noPolicyEvalResultTitle: You have no policy evaluations
accountDetails: Account Details noPolicyEvalResult: Policy evaluations are created when policy sets are enforced on your Harness entities.
accountOverview: Account Overview noSelectInput: Select appropriate options
token: Token permission:
pipelines: Pipelines noEdit: You do not have permission to edit a Policy
pipelineSettings: Pipeline Settings table:
account: Account name: Policy
settings: Settings lastModified: Last Modified
executions: Executions createdAt: Created At
addExecution: Add New Execution policiesSets:
slug: Slug newPolicyset: New Policy Set
created: Created noPolicySets: No Policy Sets evaluated
noAccount: No account? evaluationCriteria: Policy evaluation criteria
existingAccount: Already have an account? policySetSearch: Search Policy Set by name
noPolicySetTitle: Create a Policy Set to apply Policies
noPolicySet: A harness policy set allows you to group policies and configure where they will be enforced.
noPolicySetResult: No Policy Sets found
noPolicySetDescription: No Policy Set Description
stepOne:
validName: '{{$.validation.nameRequired}}'
validId: '{{$.validation.identifierRequired}}'
validIdRegex: '{{$.common.validation.formatMustBeAlphanumeric}}'
table:
name: Policy Set
enforced: Enforced
entityType: Entity Type {{name}}
event: Event
scope: Scope
entity: Entity Type
enforced: Enforced
created: Created
updated: Updated
governance:
policyAccountCount: Account ({{count}})
policyOrgCount: Organization ({{count}})
policyProjectCount: Project ({{count}})
viewPolicy: View Policy
editPolicy: Edit Policy
editPolicyMetadataTitle: Policy Name
formatInput: Format Input
selectInput: Select Input
clearOutput: Clear Output
inputFailedEvaluation: Input failed Policy Evaluation
inputSuccededEvaluation: Input succeeded Policy Evaluation
warning: warning
evaluatedTime: 'Evaluated {{time}}'
failureHeading: Pipeline execution could not proceed due to the Policy Evaluation failures.
warningHeading: Pipeline execution has Policy Evaluation warnings.
failureHeadingEvaluationDetail: Policy Evaluation failed.
warningHeadingEvaluationDetail: Policy Evaluation contains warnings.
successHeading: All policies are passed.
policySets: 'Policy Sets ({{count}})'
evaluations: Evaluations {{count}}
policySetName: 'Policy Set: {{name}}'
emptyPolicySet: This Policy Set does not have any policies attached to it.
failureModalTitle: Policy Set Evaluations
policySetsApplied: '{{pipelineName}}: Policy Sets applied'
warn: warning {{count}}
event: Pipeline Event
evaluatedOn: Evaluated On
onRun: On Run
onSave: On Save
onCreate: On Create
onStep: On Step
policyName: 'Policy Name: {{name}}'
policyIdentifier: 'Policy Identifier: {{policyIdentifier}}'
policyDescription: 'Policy Desctiption: {{policyDescription}}'
deleteTitle: Delete Policy
deleteConfirmation: Are you sure you want to delete Policy "{{name}}"? This action cannot be undone.
deleteDone: Policy "{{name}}" deleted.
deletePolicySetTitle: Delete Policy Set
deletePolicySetConfirmation: Are you sure you want to delete Policy Set "{{name}}"? This action cannot be undone.
deletePolicySetDone: Policy Set "{{name}}" deleted.
selectSamplePolicy: Select a Policy example
evaluationEmpty: No Policy is linked for this evaluation.
noPolicySetForPipeline: No Policy Set applied for this pipeline.
noEvaluationForPipeline: No Evaluation found for this pipeline.
wizard:
policyToEval: Policy to Evaluate
fieldArray: Applies to Pipeline on the following events
policySelector:
selectPolicy: Select Policy
account: Account
org: Org {{name}}
policySetGroup: Policy Set Group
policySetGroupAccount: Account {{name}}
policySetGroupOrg: Organization {{name}}
policySetGroupProject: Project {{name}}
validation:
identifierIsRequired: '{{$.validation.identifierRequired}}'
validIdRegex: Identifier must start with an letter or _ and can then be followed by alphanumerics, _, or $
thisIsARequiredField: This setting is required
nameRequired: Name is required
identifierRequired: Identifier is required
policySaveButtonMessage: '{{type}} is required'
lastUpdated: Last Updated
AZ09: A - Z, 0 - 9
ZA90: Z - A, 9 - 0
evaluation:
onePolicyEvaluated: 1 Policy Evaluated
evaluatedPoliciesCount: '{{count}} Policies Evaluated'
all: All
entity: Entity
fileOverwrite: File Overwrite
saveOverwrite: Are you sure you want to overwrite this file? Your unsaved work will be lost.
confirm: Confirm
useSample: Use This Sample
samplePolicies: Sample Policies
search: Search
input: Input
navigationCheckText: 'You have unsaved changes. Are you sure you want to leave this page without saving?'
navigationCheckTitle: 'Close without saving?'
noSearchResultsFound: No search results found for '{{searchTerm}}'.
clearFilter: Clear Filter
banner:
expired: expired {{ days }} days ago
expiryCountdown: expires in {{ days }} days

11
web/src/utils/Enums.ts Normal file
View File

@ -0,0 +1,11 @@
export enum Sort {
DESC = 'DESC',
ASC = 'ASC'
}
export enum SortFields {
LastUpdatedAt = 'updated',
AZ09 = 'AZ09',
ZA90 = 'ZA90',
Name = 'name'
}

View File

@ -0,0 +1,442 @@
import { Intent, IToaster, IToastProps, Position, Toaster } from '@blueprintjs/core'
import type { editor as EDITOR } from 'monaco-editor/esm/vs/editor/editor.api'
import { Color } from '@harness/uicore'
import { get } from 'lodash-es'
import moment from 'moment'
import { useParams } from 'react-router-dom'
import { useEffect } from 'react'
import type { StringsContextValue } from 'framework/strings/StringsContext'
import { useAppContext } from 'AppContext'
import { useStandaloneFeatureFlags } from '../hooks/useStandaloneFeatureFlags'
/** This utility shows a toaster without being bound to any component.
* It's useful to show cross-page/component messages */
export function showToaster(message: string, props?: Partial<IToastProps>): IToaster {
const toaster = Toaster.create({ position: Position.TOP })
toaster.show({ message, intent: Intent.SUCCESS, ...props })
return toaster
}
// eslint-disable-next-line
export const getErrorMessage = (error: any): string =>
get(error, 'data.error', get(error, 'data.message', error?.message))
export const MonacoEditorOptions = {
ignoreTrimWhitespace: true,
minimap: { enabled: false },
codeLens: false,
scrollBeyondLastLine: false,
smartSelect: false,
tabSize: 4,
insertSpaces: true,
overviewRulerBorder: false
}
export const MonacoEditorJsonOptions = {
...MonacoEditorOptions,
tabSize: 2
}
// Monaco editor has a bug where when its value is set, the value
// is selected all by default.
// Fix by set selection range to zero
export const deselectAllMonacoEditor = (editor?: EDITOR.IStandaloneCodeEditor): void => {
editor?.focus()
setTimeout(() => {
editor?.setSelection(new monaco.Selection(0, 0, 0, 0))
}, 0)
}
export const ENTITIES = {
pipeline: {
label: 'Pipeline',
value: 'pipeline',
eventTypes: [
{
label: 'Pipeline Evaluation',
value: 'evaluation'
}
],
actions: [
{ label: 'On Run', value: 'onrun' },
{ label: 'On Save', value: 'onsave' }
// {
// label: 'On Step',
// value: 'onstep',
// enableAction: flags => {
// return flags?.CUSTOM_POLICY_STEP
// }
// }
],
enabledFunc: flags => {
return flags?.OPA_PIPELINE_GOVERNANCE
}
},
flag: {
label: 'Feature Flag',
value: 'flag',
eventTypes: [
{
label: 'Flag Evaluation',
value: 'flag_evaluation'
}
],
actions: [{ label: 'On Save', value: 'onsave' }],
enabledFunc: flags => {
return flags?.OPA_FF_GOVERNANCE
}
},
connector: {
label: 'Connector',
value: 'connector',
eventTypes: [
{
label: 'Connector Evaluation',
value: 'connector_evaluation'
}
],
actions: [{ label: 'On Save', value: 'onsave' }],
enabledFunc: flags => {
return flags?.OPA_CONNECTOR_GOVERNANCE
}
},
secret: {
label: 'Secret',
value: 'secret',
eventTypes: [
{
label: 'On Save',
value: 'onsave'
}
],
actions: [{ label: 'On Save', value: 'onsave' }],
enabledFunc: flags => {
return flags?.OPA_SECRET_GOVERNANCE
}
},
custom: {
label: 'Custom',
value: 'custom',
eventTypes: [
{
label: 'Custom Evaluation',
value: 'custom_evaluation'
}
],
actions: [{ label: 'On Step', value: 'onstep' }],
enabledFunc: flags => {
return flags?.CUSTOM_POLICY_STEP
}
}
} as Entities
export const getEntityLabel = (entity: keyof Entities): string => {
return ENTITIES[entity].label
}
export function useEntities(): Entities {
const {
hooks: { useFeatureFlags = useStandaloneFeatureFlags }
} = useAppContext()
const flags = useFeatureFlags()
const availableEntities = { ...ENTITIES }
for (const key in ENTITIES) {
if (!ENTITIES[key as keyof Entities].enabledFunc(flags)) {
delete availableEntities[key as keyof Entities]
continue
}
// temporary(?) feature flagging of actions
availableEntities[key as keyof Entities].actions = availableEntities[key as keyof Entities].actions.filter(
action => {
return action.enableAction ? action.enableAction(flags) : true
}
)
}
return availableEntities
}
export const getActionType = (type: string | undefined, action: string | undefined): string => {
return ENTITIES[type as keyof Entities].actions.find(a => a.value === action)?.label || 'Unrecognised Action Type'
}
export type FeatureFlagMap = Partial<Record<FeatureFlag, boolean>>
export enum FeatureFlag {
OPA_PIPELINE_GOVERNANCE = 'OPA_PIPELINE_GOVERNANCE',
OPA_FF_GOVERNANCE = 'OPA_FF_GOVERNANCE',
CUSTOM_POLICY_STEP = 'CUSTOM_POLICY_STEP',
OPA_CONNECTOR_GOVERNANCE = 'OPA_CONNECTOR_GOVERNANCE',
OPA_GIT_GOVERNANCE = 'OPA_GIT_GOVERNANCE',
OPA_SECRET_GOVERNANCE = 'OPA_SECRET_GOVERNANCE'
}
export type Entity = {
label: string
value: string
eventTypes: Event[]
actions: Action[]
enabledFunc: (flags: FeatureFlagMap) => boolean
}
export type Event = {
label: string
value: string
}
export type Action = {
label: string
value: string
enableAction?: (flags: FeatureFlagMap) => boolean
}
export type Entities = {
pipeline: Entity
flag: Entity
connector: Entity
secret: Entity
custom: Entity
}
export enum EvaluationStatus {
ERROR = 'error',
PASS = 'pass',
WARNING = 'warning'
}
export const isEvaluationFailed = (status?: string): boolean =>
status === EvaluationStatus.ERROR || status === EvaluationStatus.WARNING
export const LIST_FETCHING_PAGE_SIZE = 20
// TODO - we should try and drive all these from the ENTITIES const ^ as well
// theres still a little duplication going on
export enum PipeLineEvaluationEvent {
ON_RUN = 'onrun',
ON_SAVE = 'onsave',
ON_CREATE = 'oncreate',
ON_STEP = 'onstep'
}
// TODO - we should try and drive all these from the ENTITIES const ^ as well
// theres still a little duplication going on
export enum PolicySetType {
PIPELINE = 'pipeline',
FEATURE_FLAGS = 'flag',
CUSTOM = 'custom',
CONNECTOR = 'connector'
}
export const getEvaluationEventString = (
getString: StringsContextValue['getString'],
evaluation: PipeLineEvaluationEvent
): string => {
if (!getString) return ''
const evaluations = {
onrun: getString('governance.onRun'),
onsave: getString('governance.onSave'),
oncreate: getString('governance.onCreate'),
onstep: getString('governance.onStep')
}
return evaluations[evaluation]
}
export const getEvaluationNameString = (evaluationMetadata: string): string | undefined => {
try {
const entityMetadata = JSON.parse(decodeURIComponent(evaluationMetadata as string))
if (entityMetadata.entityName) {
return entityMetadata.entityName
} else if (entityMetadata['pipelineName']) {
return entityMetadata['pipelineName'] //temporary until pipelineName is not being used
} else {
return 'Unknown'
}
} catch {
return 'Unknown'
}
}
export const evaluationStatusToColor = (status: string): Color => {
switch (status) {
case EvaluationStatus.ERROR:
return Color.ERROR
case EvaluationStatus.WARNING:
return Color.WARNING
}
return Color.SUCCESS
}
// @see https://github.com/drone/policy-mgmt/issues/270
// export const QUERY_PARAM_VALUE_ALL = '*'
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
interface SetPageNumberProps {
setPage: (value: React.SetStateAction<number>) => void
pageItemsCount?: number
page: number
}
export const setPageNumber = ({ setPage, pageItemsCount, page }: SetPageNumberProps): void => {
if (pageItemsCount === 0 && page > 0) {
setPage(page - 1)
}
}
export const ILLEGAL_IDENTIFIERS = [
'or',
'and',
'eq',
'ne',
'lt',
'gt',
'le',
'ge',
'div',
'mod',
'not',
'null',
'true',
'false',
'new',
'var',
'return'
]
export const REGO_MONACO_LANGUAGE_IDENTIFIER = 'rego'
export const omit = (originalObj = {}, keysToOmit: string[]) =>
Object.fromEntries(Object.entries(originalObj).filter(([key]) => !keysToOmit.includes(key)))
export const displayDateTime = (value: number): string | null => {
return value ? moment.unix(value / 1000).format(DEFAULT_DATE_FORMAT) : null
}
export interface GitFilterScope {
repo?: string
branch?: GitBranchDTO['branchName']
getDefaultFromOtherRepo?: boolean
}
export interface GitFiltersProps {
defaultValue?: GitFilterScope
onChange: (value: GitFilterScope) => void
className?: string
branchSelectClassName?: string
showRepoSelector?: boolean
showBranchSelector?: boolean
showBranchIcon?: boolean
shouldAllowBranchSync?: boolean
getDisabledOptionTitleText?: () => string
}
export interface GitBranchDTO {
branchName?: string
branchSyncStatus?: 'SYNCED' | 'SYNCING' | 'UNSYNCED'
}
type Module = 'cd' | 'cf' | 'ci' | undefined
export const useGetModuleQueryParam = (): Module => {
const { projectIdentifier, module } = useParams<Record<string, string>>()
return projectIdentifier ? (module as Module) : undefined
}
export enum Editions {
ENTERPRISE = 'ENTERPRISE',
TEAM = 'TEAM',
FREE = 'FREE',
COMMUNITY = 'COMMUNITY'
}
export interface License {
accountIdentifier?: string
createdAt?: number
edition?: 'COMMUNITY' | 'FREE' | 'TEAM' | 'ENTERPRISE'
expiryTime?: number
id?: string
lastModifiedAt?: number
licenseType?: 'TRIAL' | 'PAID'
moduleType?: 'CD' | 'CI' | 'CV' | 'CF' | 'CE' | 'STO' | 'CORE' | 'PMS' | 'TEMPLATESERVICE' | 'GOVERNANCE'
premiumSupport?: boolean
selfService?: boolean
startTime?: number
status?: 'ACTIVE' | 'DELETED' | 'EXPIRED'
trialExtended?: boolean
}
export interface LicenseInformation {
[key: string]: License
}
export const findEnterprisePaid = (licenseInformation: LicenseInformation): boolean => {
return !!Object.values(licenseInformation).find(
(license: License) => license.edition === Editions.ENTERPRISE && license.licenseType === 'PAID'
)
}
export const useAnyTrialLicense = (): boolean => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
const hasEnterprisePaid = findEnterprisePaid(licenseInformation)
if (hasEnterprisePaid) return false
const anyTrialEntitlements = Object.values(licenseInformation).find(
(license: License) => license?.edition === Editions.ENTERPRISE && license?.licenseType === 'TRIAL'
)
return !!anyTrialEntitlements
}
export const useGetTrialInfo = (): any => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
const hasEnterprisePaid = findEnterprisePaid(licenseInformation)
if (hasEnterprisePaid) return
const allEntitlements = Object.keys(licenseInformation).map(module => {
return licenseInformation[module]
})
const trialEntitlement = allEntitlements
.sort((a: License, b: License) => (b.expiryTime ?? 0) - (a.expiryTime ?? 0))
.find((license: License) => license?.edition === Editions.ENTERPRISE && license?.licenseType === 'TRIAL')
return trialEntitlement
}
export const useFindActiveEnterprise = (): boolean => {
const {
hooks: { useLicenseStore = () => ({}) }
} = useAppContext()
const { licenseInformation }: { licenseInformation: LicenseInformation } = useLicenseStore()
return Object.values(licenseInformation).some(
(license: License) => license.edition === Editions.ENTERPRISE && license.status === 'ACTIVE'
)
}
/**
* Scrolls the target element to top when any dependency changes
* @param {string} target Target element className selector
* @param {array} dependencies Dependencies to watch
* @returns {void}
*/
export const useScrollToTop = (target: string, dependencies: unknown[]): void => {
useEffect(() => {
const element = document.querySelector(`.${target}`)
if (element) {
element.scrollTop = 0
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dependencies])
}

467
web/src/utils/rego.ts Normal file
View File

@ -0,0 +1,467 @@
/* eslint-disable */
export const REGO_FORMAT = {
tokenPostfix: '.ruby',
keywords: [
'__LINE__',
'__ENCODING__',
'__FILE__',
'BEGIN',
'END',
'alias',
'and',
'begin',
'break',
'case',
'class',
'def',
'defined?',
'do',
'else',
'elsif',
'end',
'ensure',
'for',
'false',
'if',
'in',
'module',
'next',
'nil',
'not',
'or',
'redo',
'rescue',
'retry',
'return',
'self',
'super',
'then',
'true',
'undef',
'unless',
'until',
'when',
'while',
'yield',
'default',
'not',
'package',
'import',
'as',
'with',
'else',
'some'
],
keywordops: ['::', '..', '...', '?', ':', '=>'],
builtins: [
'require',
'public',
'private',
'include',
'extend',
'attr_reader',
'protected',
'private_class_method',
'protected_class_method',
'new'
],
// these are closed by 'end' (if, while and until are handled separately)
declarations: ['module', 'class', 'def', 'case', 'do', 'begin', 'for', 'if', 'while', 'until', 'unless'],
linedecls: ['def', 'case', 'do', 'begin', 'for', 'if', 'while', 'until', 'unless'],
operators: [
'^',
'&',
'|',
'<=>',
'==',
'===',
'!~',
'=~',
'>',
'>=',
'<',
'<=',
'<<',
'>>',
'+',
'-',
'*',
'/',
'%',
'**',
'~',
'+@',
'-@',
'[]',
'[]=',
'`',
'+=',
'-=',
'*=',
'**=',
'/=',
'^=',
'%=',
'<<=',
'>>=',
'&=',
'&&=',
'||=',
'|='
],
brackets: [
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
{ open: '{', close: '}', token: 'delimiter.curly' },
{ open: '[', close: ']', token: 'delimiter.square' }
],
// we include these common regular expressions
symbols: /[=><!~?:&|+\-*\/\^%\.]+/,
// escape sequences
escape: /(?:[abefnrstv\\"'\n\r]|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{4})/,
escapes: /\\(?:C\-(@escape|.)|c(@escape|.)|@escape)/,
decpart: /\d(_?\d)*/,
decimal: /0|@decpart/,
delim: /[^a-zA-Z0-9\s\n\r]/,
heredelim: /(?:\w+|'[^']*'|"[^"]*"|`[^`]*`)/,
regexpctl: /[(){}\[\]\$\^|\-*+?\.]/,
regexpesc: /\\(?:[AzZbBdDfnrstvwWn0\\\/]|@regexpctl|c[A-Z]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4})?/,
// The main tokenizer for our languages
tokenizer: {
// Main entry.
// root.<decl> where decl is the current opening declaration (like 'class')
root: [
// identifiers and keywords
// most complexity here is due to matching 'end' correctly with declarations.
// We distinguish a declaration that comes first on a line, versus declarations further on a line (which are most likey modifiers)
[
/^(\s*)([a-z_]\w*[!?=]?)/,
[
'white',
{
cases: {
'for|until|while': { token: 'keyword.$2', next: '@dodecl.$2' },
'@declarations': { token: 'keyword.$2', next: '@root.$2' },
end: { token: 'keyword.$S2', next: '@pop' },
'@keywords': 'keyword',
'@builtins': 'predefined',
'@default': 'identifier'
}
}
]
],
[
/[a-z_]\w*[!?=]?/,
{
cases: {
'if|unless|while|until': { token: 'keyword.$0x', next: '@modifier.$0x' },
for: { token: 'keyword.$2', next: '@dodecl.$2' },
'@linedecls': { token: 'keyword.$0', next: '@root.$0' },
end: { token: 'keyword.$S2', next: '@pop' },
'@keywords': 'keyword',
'@builtins': 'predefined',
'@default': 'identifier'
}
}
],
[/[A-Z][\w]*[!?=]?/, 'constructor.identifier'], // constant
[/\$[\w]*/, 'global.constant'], // global
[/@[\w]*/, 'namespace.instance.identifier'], // instance
[/@@[\w]*/, 'namespace.class.identifier'], // class
// here document
[/<<[-~](@heredelim).*/, { token: 'string.heredoc.delimiter', next: '@heredoc.$1' }],
[/[ \t\r\n]+<<(@heredelim).*/, { token: 'string.heredoc.delimiter', next: '@heredoc.$1' }],
[/^<<(@heredelim).*/, { token: 'string.heredoc.delimiter', next: '@heredoc.$1' }],
// whitespace
{ include: '@whitespace' },
// strings
[/"/, { token: 'string.d.delim', next: '@dstring.d."' }],
[/'/, { token: 'string.sq.delim', next: '@sstring.sq' }],
// % literals. For efficiency, rematch in the 'pstring' state
[/%([rsqxwW]|Q?)/, { token: '@rematch', next: 'pstring' }],
// commands and symbols
[/`/, { token: 'string.x.delim', next: '@dstring.x.`' }],
[/:(\w|[$@])\w*[!?=]?/, 'string.s'],
[/:"/, { token: 'string.s.delim', next: '@dstring.s."' }],
[/:'/, { token: 'string.s.delim', next: '@sstring.s' }],
// regular expressions. Lookahead for a (not escaped) closing forwardslash on the same line
[/\/(?=(\\\/|[^\/\n])+\/)/, { token: 'regexp.delim', next: '@regexp' }],
// delimiters and operators
[/[{}()\[\]]/, '@brackets'],
[
/@symbols/,
{
cases: {
'@keywordops': 'keyword',
'@operators': 'operator',
'@default': ''
}
}
],
[/[;,]/, 'delimiter'],
// numbers
[/0[xX][0-9a-fA-F](_?[0-9a-fA-F])*/, 'number.hex'],
[/0[_oO][0-7](_?[0-7])*/, 'number.octal'],
[/0[bB][01](_?[01])*/, 'number.binary'],
[/0[dD]@decpart/, 'number'],
[
/@decimal((\.@decpart)?([eE][\-+]?@decpart)?)/,
{
cases: {
$1: 'number.float',
'@default': 'number'
}
}
]
],
// used to not treat a 'do' as a block opener if it occurs on the same
// line as a 'do' statement: 'while|until|for'
// dodecl.<decl> where decl is the declarations started, like 'while'
dodecl: [
[/^/, { token: '', switchTo: '@root.$S2' }], // get out of do-skipping mode on a new line
[
/[a-z_]\w*[!?=]?/,
{
cases: {
end: { token: 'keyword.$S2', next: '@pop' }, // end on same line
do: { token: 'keyword', switchTo: '@root.$S2' }, // do on same line: not an open bracket here
'@linedecls': { token: '@rematch', switchTo: '@root.$S2' }, // other declaration on same line: rematch
'@keywords': 'keyword',
'@builtins': 'predefined',
'@default': 'identifier'
}
}
],
{ include: '@root' }
],
// used to prevent potential modifiers ('if|until|while|unless') to match
// with 'end' keywords.
// modifier.<decl>x where decl is the declaration starter, like 'if'
modifier: [
[/^/, '', '@pop'], // it was a modifier: get out of modifier mode on a new line
[
/[a-z_]\w*[!?=]?/,
{
cases: {
end: { token: 'keyword.$S2', next: '@pop' }, // end on same line
'then|else|elsif|do': { token: 'keyword', switchTo: '@root.$S2' }, // real declaration and not a modifier
'@linedecls': { token: '@rematch', switchTo: '@root.$S2' }, // other declaration => not a modifier
'@keywords': 'keyword',
'@builtins': 'predefined',
'@default': 'identifier'
}
}
],
{ include: '@root' }
],
// single quote strings (also used for symbols)
// sstring.<kind> where kind is 'sq' (single quote) or 's' (symbol)
sstring: [
[/[^\\']+/, 'string.$S2'],
[/\\\\|\\'|\\$/, 'string.$S2.escape'],
[/\\./, 'string.$S2.invalid'],
[/'/, { token: 'string.$S2.delim', next: '@pop' }]
],
// double quoted "string".
// dstring.<kind>.<delim> where kind is 'd' (double quoted), 'x' (command), or 's' (symbol)
// and delim is the ending delimiter (" or `)
dstring: [
[/[^\\`"#]+/, 'string.$S2'],
[/#/, 'string.$S2.escape', '@interpolated'],
[/\\$/, 'string.$S2.escape'],
[/@escapes/, 'string.$S2.escape'],
[/\\./, 'string.$S2.escape.invalid'],
[
/[`"]/,
{
cases: {
'$#==$S3': { token: 'string.$S2.delim', next: '@pop' },
'@default': 'string.$S2'
}
}
]
],
// literal documents
// heredoc.<close> where close is the closing delimiter
heredoc: [
[
/^(\s*)(@heredelim)$/,
{
cases: {
'$2==$S2': ['string.heredoc', { token: 'string.heredoc.delimiter', next: '@pop' }],
'@default': ['string.heredoc', 'string.heredoc']
}
}
],
[/.*/, 'string.heredoc']
],
// interpolated sequence
interpolated: [
[/\$\w*/, 'global.constant', '@pop'],
[/@\w*/, 'namespace.class.identifier', '@pop'],
[/@@\w*/, 'namespace.instance.identifier', '@pop'],
[/[{]/, { token: 'string.escape.curly', switchTo: '@interpolated_compound' }],
['', '', '@pop'] // just a # is interpreted as a #
],
// any code
interpolated_compound: [[/[}]/, { token: 'string.escape.curly', next: '@pop' }], { include: '@root' }],
// %r quoted regexp
// pregexp.<open>.<close> where open/close are the open/close delimiter
pregexp: [
{ include: '@whitespace' },
// turns out that you can quote using regex control characters, aargh!
// for example; %r|kgjgaj| is ok (even though | is used for alternation)
// so, we need to match those first
[
/[^\(\{\[\\]/,
{
cases: {
'$#==$S3': { token: 'regexp.delim', next: '@pop' },
'$#==$S2': { token: 'regexp.delim', next: '@push' }, // nested delimiters are allowed..
'~[)}\\]]': '@brackets.regexp.escape.control',
'~@regexpctl': 'regexp.escape.control',
'@default': 'regexp'
}
}
],
{ include: '@regexcontrol' }
],
// We match regular expression quite precisely
regexp: [{ include: '@regexcontrol' }, [/[^\\\/]/, 'regexp'], ['/[ixmp]*', { token: 'regexp.delim' }, '@pop']],
regexcontrol: [
[
/(\{)(\d+(?:,\d*)?)(\})/,
['@brackets.regexp.escape.control', 'regexp.escape.control', '@brackets.regexp.escape.control']
],
[/(\[)(\^?)/, ['@brackets.regexp.escape.control', { token: 'regexp.escape.control', next: '@regexrange' }]],
[/(\()(\?[:=!])/, ['@brackets.regexp.escape.control', 'regexp.escape.control']],
[/\(\?#/, { token: 'regexp.escape.control', next: '@regexpcomment' }],
[/[()]/, '@brackets.regexp.escape.control'],
[/@regexpctl/, 'regexp.escape.control'],
[/\\$/, 'regexp.escape'],
[/@regexpesc/, 'regexp.escape'],
[/\\\./, 'regexp.invalid'],
[/#/, 'regexp.escape', '@interpolated']
],
regexrange: [
[/-/, 'regexp.escape.control'],
[/\^/, 'regexp.invalid'],
[/\\$/, 'regexp.escape'],
[/@regexpesc/, 'regexp.escape'],
[/[^\]]/, 'regexp'],
[/\]/, '@brackets.regexp.escape.control', '@pop']
],
regexpcomment: [
[/[^)]+/, 'comment'],
[/\)/, { token: 'regexp.escape.control', next: '@pop' }]
],
// % quoted strings
// A bit repetitive since we need to often special case the kind of ending delimiter
pstring: [
[/%([qws])\(/, { token: 'string.$1.delim', switchTo: '@qstring.$1.(.)' }],
[/%([qws])\[/, { token: 'string.$1.delim', switchTo: '@qstring.$1.[.]' }],
[/%([qws])\{/, { token: 'string.$1.delim', switchTo: '@qstring.$1.{.}' }],
[/%([qws])</, { token: 'string.$1.delim', switchTo: '@qstring.$1.<.>' }],
[/%([qws])(@delim)/, { token: 'string.$1.delim', switchTo: '@qstring.$1.$2.$2' }],
[/%r\(/, { token: 'regexp.delim', switchTo: '@pregexp.(.)' }],
[/%r\[/, { token: 'regexp.delim', switchTo: '@pregexp.[.]' }],
[/%r\{/, { token: 'regexp.delim', switchTo: '@pregexp.{.}' }],
[/%r</, { token: 'regexp.delim', switchTo: '@pregexp.<.>' }],
[/%r(@delim)/, { token: 'regexp.delim', switchTo: '@pregexp.$1.$1' }],
[/%(x|W|Q?)\(/, { token: 'string.$1.delim', switchTo: '@qqstring.$1.(.)' }],
[/%(x|W|Q?)\[/, { token: 'string.$1.delim', switchTo: '@qqstring.$1.[.]' }],
[/%(x|W|Q?)\{/, { token: 'string.$1.delim', switchTo: '@qqstring.$1.{.}' }],
[/%(x|W|Q?)</, { token: 'string.$1.delim', switchTo: '@qqstring.$1.<.>' }],
[/%(x|W|Q?)(@delim)/, { token: 'string.$1.delim', switchTo: '@qqstring.$1.$2.$2' }],
[/%([rqwsxW]|Q?)./, { token: 'invalid', next: '@pop' }], // recover
[/./, { token: 'invalid', next: '@pop' }] // recover
],
// non-expanded quoted string.
// qstring.<kind>.<open>.<close>
// kind = q|w|s (single quote, array, symbol)
// open = open delimiter
// close = close delimiter
qstring: [
[/\\$/, 'string.$S2.escape'],
[/\\./, 'string.$S2.escape'],
[
/./,
{
cases: {
'$#==$S4': { token: 'string.$S2.delim', next: '@pop' },
'$#==$S3': { token: 'string.$S2.delim', next: '@push' }, // nested delimiters are allowed..
'@default': 'string.$S2'
}
}
]
],
// expanded quoted string.
// qqstring.<kind>.<open>.<close>
// kind = Q|W|x (double quote, array, command)
// open = open delimiter
// close = close delimiter
qqstring: [[/#/, 'string.$S2.escape', '@interpolated'], { include: '@qstring' }],
// whitespace & comments
whitespace: [
[/[ \t\r\n]+/, ''],
[/^\s*=begin\b/, 'comment', '@comment'],
[/#.*$/, 'comment']
],
comment: [
[/[^=]+/, 'comment'],
[/^\s*=begin\b/, 'comment.invalid'], // nested comment
[/^\s*=end\b.*/, 'comment', '@pop'],
[/[=]/, 'comment']
]
}
}
export const REGO_THEME = {
base: 'vs-dark',
inherit: true,
colors: {
'editor.background': '#4F5162'
}
}

File diff suppressed because it is too large Load Diff