Merge pull request #1 from harness/ui-tempalate-update

Sync UI template with latest updates
This commit is contained in:
Tan Nhu 2022-08-18 11:45:03 -07:00 committed by GitHub
commit 6c65018ccf
70 changed files with 11728 additions and 3276 deletions

View File

@ -35,6 +35,12 @@ settings:
typescript:
alwaysTryTypes: true
rules:
'@typescript-eslint/ban-types':
- error
- extendDefaults: true
types:
'{}': false
# custom rules
no-document-body-snapshot: 2
duplicate-data-tooltip-id: 'warn'

View File

@ -0,0 +1,44 @@
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'
},
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')
})
})

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 (
"embed"
"io/fs"
"net/http"
"path/filepath"
)
//go:embed dist/*
var content 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)
})
}
var UI embed.FS

View File

@ -1,5 +1,5 @@
{
"name": "sample-module",
"name": "ui-template",
"description": "Harness Inc",
"version": "0.0.1",
"author": "Harness Inc",
@ -8,14 +8,14 @@
"homepage": "http://harness.io/",
"repository": {
"type": "git",
"url": "https://github.com/drone/sample-module.git"
"url": "https://github.com/wings-software/ui-template.git"
},
"bugs": {
"url": "https://github.com/sample-module/sample-module/issues"
"url": "https://github.com/wings-software/ui-template/issues"
},
"keywords": [],
"scripts": {
"dev": "NODE_ENV=development webpack serve --progress",
"dev": "webpack serve --config config/webpack.dev.js",
"test": "jest src --silent",
"test:watch": "jest --watch",
"lint": "eslint --rulesdir ./scripts/eslint-rules --ext .ts --ext .tsx src",
@ -24,60 +24,130 @@
"services": "npm-run-all services:*",
"services:pm": "restful-react import --config restful-react.config.js pm",
"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",
"setup-github-registry": "sh scripts/setup-github-registry.sh",
"strings": "npm-run-all strings:*",
"strings:genTypes": "node scripts/strings/generateTypesCli.mjs",
"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",
"micro:build": "webpack --mode production",
"micro:serve": "serve ./dist -l 3000"
"checks": "npm run typecheck; npm run lint; npm run test"
},
"dependencies": {
"@blueprintjs/core": "3.26.1",
"@blueprintjs/datetime": "3.13.0",
"@blueprintjs/select": "3.12.3",
"@harness/uicore": "^1.23.0",
"anser": "^2.1.0",
"classnames": "^2.3.1",
"@emotion/core": "^10.0.28",
"@emotion/styled": "^10.0.27",
"@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",
"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",
"jsonc-parser": "^2.0.2",
"lodash-es": "^4.17.15",
"marked": "^3.0.8",
"marked": "^4.0.12",
"masonry-layout": "^4.2.2",
"ml-matrix": "^6.5.0",
"moment": "^2.25.3",
"moment-range": "^4.0.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",
"react": "^17.0.2",
"react-beautiful-dnd": "^13.0.0",
"react-contenteditable": "^3.3.5",
"react-dom": "^17.0.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-sticky": "^1.1.3",
"react-timeago": "^4.4.0",
"react-virtuoso": "^1.10.2",
"restful-react": "15.6.0",
"swr": "^0.5.4",
"yaml": "^1.10.0"
"secure-web-storage": "^1.0.2",
"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": {
"@harness/css-types-loader": "^3.1.0",
"@harness/jarvis": "0.12.0",
"@babel/core": "^7.13.15",
"@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/react": "^10.0.3",
"@testing-library/react-hooks": "5",
"@testing-library/user-event": "^10.3.1",
"@types/classnames": "^2.2.10",
"@types/jest": "^26.0.15",
"@types/lodash-es": "^4.17.3",
"@types/masonry-layout": "^4.2.1",
"@types/mustache": "^4.0.1",
"@types/node": "^16.4.10",
"@types/path-to-regexp": "^1.7.0",
"@types/qs": "^6.9.4",
"@types/query-string": "^6.3.0",
"@types/react": "^17.0.3",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.3",
"@types/react-monaco-editor": "^0.16.0",
"@types/react-router-dom": "^5.1.7",
"@types/react-table": "^7.0.18",
"@types/react-timeago": "^4.1.1",
"@types/testing-library__react-hooks": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"@types/testing-library__user-event": "^4.1.1",
"@types/uuid": "^8.3.0",
"@types/yup": "^0.29.0",
"@typescript-eslint/eslint-plugin": "^5.33.1",
"@typescript-eslint/parser": "^5.33.1",
"@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",
"circular-dependency-plugin": "^5.2.2",
"css-loader": "^6.3.0",
@ -89,12 +159,23 @@
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-react": "^7.23.2",
"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",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.1",
"glob": "^7.1.6",
"graphql": "^15.5.0",
"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-canvas-mock": "^2.3.0",
"jest-junit": "^12.0.0",
"lighthouse": "^6.5.0",
"lint-staged": "^11.0.0",
"mini-css-extract-plugin": "^2.4.2",
@ -102,22 +183,30 @@
"mustache": "^4.0.1",
"nodemon": "^2.0.15",
"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",
"postinstall-postinstall": "^2.1.0",
"prettier": "^2.3.2",
"react-test-renderer": "^17.0.2",
"sass": "^1.32.8",
"sass-loader": "^12.1.0",
"serve": "^13.0.2",
"source-map-support": "^0.5.20",
"style-loader": "^3.3.0",
"ts-jest": "^26.5.5",
"ts-loader": "^9.2.6",
"ts-node": "^10.2.1",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.2.4",
"typescript": "^4.7.4",
"url-loader": "^4.1.1",
"webpack": "^5.58.0",
"webpack-bugsnag-plugins": "^1.8.0",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0",
"webpack-cli": "^4.9.0",
"webpack-dev-server": "^4.3.1",
"worker-loader": "^3.0.8",
"yaml-loader": "^0.6.0"
},
"resolutions": {
@ -126,7 +215,11 @@
"@types/testing-library__react": "^10.0.0",
"@types/testing-library__dom": "^7.0.0",
"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": {
"node": ">=14.16.0"

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
import type React from 'react'
import type * as History from 'history'
import type { PermissionOptionsMenuButtonProps } from 'components/Permissions/PermissionsOptionsMenuButton'
import type { LangLocale } from './framework/strings/languageLoader'
import type { FeatureFlagMap, GitFiltersProps } from './utils/GovernanceUtils'
/**
* AppProps defines an interface for host (parent) and
@ -7,7 +10,6 @@ import type { LangLocale } from './framework/strings/languageLoader'
* of the child app to be customized from the parent app.
*
* Areas of customization:
*
* - API token
* - Active user
* - Active locale (i18n)
@ -59,15 +61,27 @@ export interface AppPathProps {
policyIdentifier?: string
policySetIdentifier?: string
evaluationId?: string
pipeline?: string
execution?: string
repo?: string
branch?: string
}
/**
* AppPropsHook defines a collection of React Hooks that application receives from
* 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
@ -75,4 +89,20 @@ export interface AppPropsHook {} // eslint-disable-line @typescript-eslint/no-e
*/
export interface AppPropsComponent {
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
}>
}

View File

@ -3,37 +3,103 @@ import type { AppPathProps } from 'AppProps'
export enum RoutePath {
SIGNIN = '/signin',
TEST_PAGE1 = '/test-page1',
TEST_PAGE2 = '/test-page2',
SIGNUP = '/signup',
REGISTER = '/register',
LOGIN = '/login',
USERS = '/users',
ACCOUNT = '/account',
PIPELINES = '/pipelines',
PIPELINE = '/pipelines/:pipeline',
PIPELINE_SETTINGS = '/pipelines/:pipeline/settings',
PIPELINE_EXECUTIONS = '/pipelines/:pipeline/executions',
PIPELINE_EXECUTION = '/pipelines/:pipeline/executions/:execution',
PIPELINE_EXECUTION_SETTINGS = '/pipelines/:pipeline/executions/:execution/settings'
POLICY_DASHBOARD = '/dashboard',
POLICY_LISTING = '/policies',
POLICY_NEW = '/policies/new',
POLICY_VIEW = '/policies/view/:policyIdentifier',
//POLICY_EDIT = '/policies/edit/:policyIdentifier',
POLICY_EDIT = '/policies/edit/:policyIdentifier/:repo?/:branch?',
POLICY_SETS_LISTING = '/policy-sets',
POLICY_SETS_DETAIL = '/policy-sets/:policySetIdentifier',
POLICY_EVALUATIONS_LISTING = '/policy-evaluations',
POLICY_EVALUATION_DETAIL = '/policy-evaluations/:evaluationId'
}
export default {
toLogin: (): string => toRouteURL(RoutePath.LOGIN),
toSignIn: (): string => toRouteURL(RoutePath.SIGNIN),
toSignUp: (): string => toRouteURL(RoutePath.SIGNUP),
toRegister: (): string => toRouteURL(RoutePath.REGISTER),
toAccount: (): string => toRouteURL(RoutePath.ACCOUNT),
toPipelines: (): string => toRouteURL(RoutePath.PIPELINES),
toPipeline: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string =>
toRouteURL(RoutePath.PIPELINE, { pipeline }),
toPipelineExecutions: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string =>
toRouteURL(RoutePath.PIPELINE_EXECUTIONS, { pipeline }),
toPipelineSettings: ({ pipeline }: Required<Pick<AppPathProps, 'pipeline'>>): string =>
toRouteURL(RoutePath.PIPELINE_SETTINGS, { pipeline }),
toPipelineExecution: ({ pipeline, execution }: AppPathProps): string =>
toRouteURL(RoutePath.PIPELINE_EXECUTION, { pipeline, execution }),
toPipelineExecutionSettings: ({ pipeline, execution }: AppPathProps): string =>
toRouteURL(RoutePath.PIPELINE_EXECUTION_SETTINGS, { pipeline, execution })
// @see https://github.com/drone/policy-mgmt/blob/main/web/src/RouteDefinitions.ts
// for more examples regarding to passing parameters to generate URLs
toPolicyDashboard: (): string => toRouteURL(RoutePath.POLICY_DASHBOARD),
toPolicyListing: (): string => toRouteURL(RoutePath.POLICY_LISTING),
toPolicyNew: (): string => toRouteURL(RoutePath.POLICY_NEW),
toPolicyView: ({ policyIdentifier }: Required<Pick<AppPathProps, 'policyIdentifier'>>): string =>
toRouteURL(RoutePath.POLICY_VIEW, { policyIdentifier }),
toPolicyEdit: ({ policyIdentifier }: Required<Pick<AppPathProps, 'policyIdentifier'>>): string =>
toRouteURL(RoutePath.POLICY_EDIT, { policyIdentifier }),
toPolicySets: (): string => toRouteURL(RoutePath.POLICY_SETS_LISTING),
toPolicyEvaluations: (): string => toRouteURL(RoutePath.POLICY_EVALUATIONS_LISTING),
toGovernancePolicyDashboard: ({ orgIdentifier, projectIdentifier, module }: AppPathProps) =>
toRouteURL(RoutePath.POLICY_DASHBOARD, {
orgIdentifier,
projectIdentifier,
module
}),
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,68 +1,69 @@
import React from 'react'
import { HashRouter, Route, Switch } from 'react-router-dom'
import type { AppProps } from 'AppProps'
/* eslint-disable react/display-name */
import React, { useCallback } from 'react'
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
// import { SignInPage } from 'pages/signin/SignInPage'
import { NotFoundPage } from 'pages/404/NotFoundPage'
import { routePath } from 'RouteUtils'
import { RoutePath } from 'RouteDefinitions'
import { SignIn } from 'pages/SignIn/SignIn'
import { Register } from 'pages/Register/Register'
import { routePath, standaloneRoutePath } from './RouteUtils'
import { RoutePath } from './RouteDefinitions'
import { Login } from './pages/Login/Login'
import { Home } from './pages/Pipelines/Pipelines'
import { Executions } from './pages/Executions/Executions'
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<{ standalone: boolean }> = React.memo(({ standalone }) => {
const Destinations: React.FC = useCallback(
() => (
<Switch>
{standalone && (
<>
<Route path={routePath(RoutePath.SIGNIN)}>
<SignIn />
</Route>
<Route path={routePath(RoutePath.SIGNUP)}>
<SignIn />
</Route>
<Route path={routePath(RoutePath.REGISTER)}>
<Register />
</Route>
</>
)}
export const RouteDestinations: React.FC<Pick<AppProps, 'standalone'>> = ({ standalone }) => {
// TODO: Add a generic Auth Wrapper
const Destinations: React.FC = () => (
<Switch>
{standalone && (
<Route path={routePath(RoutePath.REGISTER)}>
<Login />
<Route path={routePath(RoutePath.POLICY_DASHBOARD)}>
<h1>Overview</h1>
</Route>
)}
{standalone && (
<Route path={routePath(RoutePath.LOGIN)}>
<Login />
<Route path={routePath(RoutePath.POLICY_NEW)}>
<h1>New</h1>
</Route>
)}
<Route exact path={routePath(RoutePath.PIPELINES)}>
<SideNav>
<Home />
</SideNav>
</Route>
<Route path={routePath(RoutePath.POLICY_VIEW)}>
<h1>View</h1>
</Route>
<Route exact path={routePath(RoutePath.PIPELINE)}>
<SideNav>
<Executions />
</SideNav>
</Route>
<Route exact path={routePath(RoutePath.POLICY_EDIT)}>
<h1>Edit</h1>
</Route>
<Route exact path={routePath(RoutePath.PIPELINE_SETTINGS)}>
<SideNav>
<PipelineSettings />
</SideNav>
</Route>
<Route path={routePath(RoutePath.POLICY_LISTING)}>
<h1>Listing</h1>
</Route>
<Route exact path={routePath(RoutePath.PIPELINE_EXECUTION_SETTINGS)}>
<SideNav>
<ExecutionSettings />
</SideNav>
</Route>
<Route exact path={routePath(RoutePath.POLICY_SETS_LISTING)}>
<h1>Listing 2</h1>
</Route>
<Route exact path={routePath(RoutePath.ACCOUNT)}>
<SideNav>
<Account />
</SideNav>
</Route>
<Route path={routePath(RoutePath.POLICY_SETS_DETAIL)}>
<h1>Detail 1</h1>
</Route>
<Route path="/">
<NotFoundPage />
</Route>
</Switch>
<Route path={routePath(RoutePath.POLICY_EVALUATION_DETAIL)}>
<h1>Detail 2</h1>
</Route>
<Route path="/">
{standalone ? <Redirect to={standaloneRoutePath(RoutePath.POLICY_DASHBOARD)} /> : <NotFoundPage />}
</Route>
</Switch>
),
[standalone]
)
return standalone ? (
@ -72,4 +73,4 @@ export const RouteDestinations: React.FC<Pick<AppProps, 'standalone'>> = ({ stan
) : (
<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
// 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 => {
if (window.APP_RUN_IN_STANDALONE_MODE) {
return path
}
const { orgIdentifier, projectIdentifier, module } = scope
//
// TODO: Change this scheme below to reflect your application when it's embedded into Harness NextGen UI
//
// 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)
// The Governance app is mounted in three places in Harness Platform
// 1. Account Settings (account level governance)
// 2. Org Details (org level governance)
// 3. Project Settings (project level governance)
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) {
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) {
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
* @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)

View File

@ -1,10 +1,21 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './App.scss'
// This flag is used in services/config.ts to customize API path when app is run
// in multiple modes (standalone vs. embedded).
// Also being used in when generating proper URLs inside the app.
window.APP_RUN_IN_STANDALONE_MODE = true
window.STRIP_SCM_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 => {
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 }
return (
<FormInput.InputWithIdentifier inputLabel={nameLabel} inputGroupProps={newInputGroupProps} {...identifierProps} />
@ -55,9 +55,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => {
return (
<Container style={{ marginBottom: isDescriptionOpen ? '0' : 'var(--spacing-medium)' }}>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional
? getString('common.optionalField', { name: getString('common.description') })
: getString('common.description')}
{isOptional ? getString('optionalField', { name: getString('description') }) : getString('description')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isDescriptionOpen && (
<Icon
@ -79,7 +77,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => {
disabled={disabled}
autoFocus={isDescriptionFocus}
name="description"
placeholder={getString('common.descriptionPlaceholder')}
placeholder={getString('descriptionPlaceholder')}
{...restDescriptionProps}
/>
)}
@ -95,9 +93,7 @@ export const Tags = (props: TagsComponentProps): JSX.Element => {
return (
<Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)} data-tooltip-id={props.dataTooltipId}>
{isOptional
? getString('common.optionalField', { name: getString('common.tagsLabel') })
: getString('common.tagsLabel')}
{isOptional ? getString('optionalField', { name: getString('tagsLabel') }) : getString('tagsLabel')}
{props.dataTooltipId ? <HarnessDocTooltip useStandAlone={true} tooltipId={props.dataTooltipId} /> : null}
{!isTagsOpen && (
<Icon
@ -125,7 +121,7 @@ function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
return (
<Container>
<Label className={cx(Classes.LABEL, css.descriptionLabel)}>
{getString('common.tagsLabel')}
{getString('tagsLabel')}
{!isTagsOpen && (
<Icon
className={css.editOpen}
@ -159,15 +155,7 @@ function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element {
const { getString } = useStrings()
const {
className,
identifierProps,
descriptionProps,
tagsProps,
formikProps,
inputGroupProps = {},
tooltipProps
} = props
const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {}, tooltipProps } = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
return (
<Container className={cx(css.main, className)}>
@ -177,12 +165,6 @@ export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.El
hasValue={!!formikProps?.values.description}
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>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import { Classes, Menu } from '@blueprintjs/core'
import React, { ReactElement } from 'react'
import { Classes, IMenuItemProps, Menu } from '@blueprintjs/core'
import { Button, ButtonProps } from '@harness/uicore'
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> | '-'>
}
export const OptionsMenuButton: React.FC<OptionsMenuButtonProps> = ({ items, ...props }) => {
export const OptionsMenuButton = ({ items, ...props }: OptionsMenuButtonProps): ReactElement => {
return (
<Button
minimal
@ -23,7 +23,7 @@ export const OptionsMenuButton: React.FC<OptionsMenuButtonProps> = ({ items, ...
<Menu.Item
key={(item as React.ComponentProps<typeof Menu.Item>)?.text as string}
className={Classes.POPOVER_DISMISS}
{...item}
{...(item as IMenuItemProps & React.AnchorHTMLAttributes<HTMLAnchorElement>)}
/>
)
)}

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 */
// this is an auto-generated file
declare const styles: {
readonly spinner: string
readonly loadingSpinnerWrapper: string
readonly hidden: string
}
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

@ -1,7 +1,6 @@
import React from 'react'
import mustache from 'mustache'
import { get } from 'lodash-es'
import { useStringsContext, StringKeys } from './StringsContext'
export interface UseStringsReturn {
@ -46,9 +45,9 @@ export function String(props: StringProps): React.ReactElement | null {
const text = getString(stringID, vars)
return useRichText ? (
<Tag {...(rest as unknown)} dangerouslySetInnerHTML={{ __html: text }} />
<Tag {...(rest as unknown as {})} dangerouslySetInnerHTML={{ __html: text }} />
) : (
<Tag {...(rest as unknown)}>{text}</Tag>
<Tag {...(rest as unknown as {})}>{text}</Tag>
)
} catch (e) {
if (process.env.NODE_ENV !== 'production') {

View File

@ -3,37 +3,150 @@
* Use the command `yarn strings` to regenerate this file.
*/
export interface StringsMap {
account: string
addExecution: string
'common.accountDetails': string
'common.accountOverview': string
'common.cancel': string
'common.delete': string
'common.deleteConfirm': string
'common.description': string
'common.descriptionPlaceholder': string
'common.edit': string
'common.email': string
'common.itemCreated': string
'common.itemDeleted': string
'common.itemUpdated': string
'common.name': string
AZ09: string
ZA90: string
action: string
all: string
apply: string
back: string
'banner.expired': string
'banner.expiryCountdown': string
cancel: string
clearFilter: string
'common.namePlaceholder': string
'common.optionalField': string
'common.save': string
'common.tagsLabel': string
'common.token': string
created: string
executions: string
'common.policies': string
'common.policiesSets.created': string
'common.policiesSets.enforced': string
'common.policiesSets.entity': string
'common.policiesSets.evaluationCriteria': string
'common.policiesSets.event': string
'common.policiesSets.newPolicyset': string
'common.policiesSets.noPolicySet': string
'common.policiesSets.noPolicySetDescription': string
'common.policiesSets.noPolicySetResult': string
'common.policiesSets.noPolicySetTitle': string
'common.policiesSets.noPolicySets': string
'common.policiesSets.policySetSearch': string
'common.policiesSets.scope': string
'common.policiesSets.stepOne.validId': string
'common.policiesSets.stepOne.validIdRegex': 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
email: string
entity: string
'evaluation.evaluatedPoliciesCount': string
'evaluation.onePolicyEvaluated': string
executionsText: string
existingAccount: string
logout: 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
noAccount: string
noSearchResultsFound: string
optionalField: string
outputLabel: string
overview: string
pageNotFound: string
password: string
pipelineSettings: string
pipelines: string
settings: string
samplePolicies: string
saveOverwrite: string
search: string
signIn: string
signUp: string
signin: string
slug: 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 */
declare const __DEV__: boolean
declare const __ON_PREM__: boolean
declare const __ON_PREM__: booelan
declare module '*.png' {
const value: string
@ -45,7 +45,7 @@ declare module '*.gql' {
declare interface Window {
apiUrl: string
bugsnagClient?: any
APP_RUN_IN_STANDALONE_MODE?: boolean
STRIP_SCM_PREFIX?: boolean
}
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,159 @@
signin: Sign In
signUp: Sign Up
logout: Logout
password: Password
pageNotFound: Page Not Found
signIn: Sign In
signUp: Sign Up
email: Email
password: Password
noAccount: No Account
existingAccount: Existing Account
failed: Failed
status: Status
success: Success
details: Details
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:
save: Save
edit: Edit
name: Name
email: Email
namePlaceholder: Enter Name
description: Description
descriptionPlaceholder: Enter Description
tagsLabel: Tags
optionalField: '{{name}} (optional)'
delete: Delete
deleteConfirm: Are you sure you want to delete this?
itemDeleted: Item Deleted
itemUpdated: Item Updated
itemCreated: Item Created
cancel: Cancel
accountDetails: Account Details
accountOverview: Account Overview
token: Token
pipelines: Pipelines
pipelineSettings: Pipeline Settings
account: Account
settings: Settings
executions: Executions
addExecution: Add New Execution
slug: Slug
created: Created
noAccount: No account?
existingAccount: Already have an account?
policies: 'Policies'
policy:
policysets: Policy Sets
newPolicy: New Policy
evaluations: Evaluations
policySearch: Search Policy by name
noPolicy: A Harness policy is an OPA rule that can be enforced on your Harness software delivery processes to ensure governance and compliance.
noPolicyTitle: You have no policies
noPolicyResult: No policies found
noPolicyEvalResultTitle: You have no policy evaluations
noPolicyEvalResult: Policy evaluations are created when policy sets are enforced on your Harness entities.
noSelectInput: Select appropriate options
permission:
noEdit: You do not have permission to edit a Policy
table:
name: Policy
lastModified: Last Modified
createdAt: Created At
policiesSets:
newPolicyset: New Policy Set
noPolicySets: No Policy Sets evaluated
evaluationCriteria: Policy evaluation criteria
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

View File

@ -1,15 +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 root: string
}
export default styles

View File

@ -1,153 +0,0 @@
import React, { useState, useEffect } from 'react'
import { useHistory } from 'react-router-dom'
import {
Container,
Button,
Formik,
FormikForm,
FormInput,
Text,
Color,
Layout,
ButtonVariation,
Page,
useToaster
} from '@harness/uicore'
import { useUpdateUser, useGetUser } from 'services/pm'
import { useStrings } from 'framework/strings'
import routes from 'RouteDefinitions'
import styles from './Account.module.scss'
interface UserFormProps {
name: string
email: string
password1: string
password2: string
}
export const Account = () => {
const history = useHistory()
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
const { data, loading, error, refetch } = useGetUser({})
const { mutate } = useUpdateUser({})
const [editDetails, setEditDetails] = useState(false)
const [name, setName] = useState<string | undefined>('')
const [email, setEmail] = useState<string | undefined>('')
useEffect(() => {
setName(data?.name)
setEmail(data?.email)
}, [data])
if (error) {
history.push(routes.toLogin())
}
const updateUserDetails = async ({ email, name, password1 }: UserFormProps) => {
try {
await mutate({ email, name, password: password1 })
showSuccess(getString('common.itemUpdated'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error({ err })
}
}
const handleSubmit = (data: UserFormProps): void => {
setEditDetails(false)
updateUserDetails(data)
}
const editUserForm = (
<Formik<UserFormProps>
initialValues={{ name: name as string, email: email as string, password1: '', password2: '' }}
formName="newPipelineForm"
onSubmit={handleSubmit}>
<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.email')}
</Text>
<FormInput.Text name="email" 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('password')}
</Text>
<FormInput.Text
name="password1"
label="Password"
inputGroup={{ type: 'password' }}
className={styles.input}
/>
<FormInput.Text
name="password2"
label="Re-type your Password"
inputGroup={{ type: 'password' }}
className={styles.input}
/>
</Layout.Horizontal>
<Button variation={ButtonVariation.LINK} icon="updated" text={getString('common.save')} type="submit" />
</FormikForm>
</Formik>
)
return (
<Container className={styles.root} height="inherit">
<Page.Header title={getString('common.accountOverview')} />
<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' }}>
{getString('common.accountDetails')}
</Text>
{editDetails ? (
editUserForm
) : (
<>
<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.email')}</Text>
<Text color={Color.GREY_800}>{email}</Text>
</Layout.Horizontal>
<Layout.Horizontal
flex={{ alignItems: 'center', justifyContent: 'flex-start' }}
margin={{ bottom: 'large' }}>
<Text className={styles.minWidth}>{getString('password')}</Text>
<Text padding={{ right: 'small' }} color={Color.GREY_800}>
*********
</Text>
</Layout.Horizontal>
<Button
variation={ButtonVariation.LINK}
icon="Edit"
text={getString('common.edit')}
onClick={() => setEditDetails(true)}
/>
</>
)}
</Container>
</Page.Body>
</Container>
)
}

View File

@ -1,17 +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;
}

View File

@ -1,100 +0,0 @@
import React, { useState, useEffect } from 'react'
import { useParams, useHistory } from 'react-router-dom'
import { startCase, camelCase } from 'lodash'
import { useToaster, useConfirmationDialog, Text, Color } from '@harness/uicore'
import { Intent } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import { useGetExecution, useDeleteExecution, useUpdateExecution } from 'services/pm'
import { Settings } from '../../components/Settings/Settings'
import routes from 'RouteDefinitions'
interface PathProps {
pipeline: string
execution: string
}
interface ExecutionProps {
name?: string
desc?: string
}
export const ExecutionSettings = () => {
const history = useHistory()
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const { pipeline, execution } = useParams<PathProps>()
const [name, setName] = useState<string | undefined>('')
const [desc, setDesc] = useState<string | undefined>('')
const { data, loading, error, refetch } = useGetExecution({ pipeline, execution })
const { mutate: deleteExecution } = useDeleteExecution({ pipeline })
const { mutate: updateExecution } = useUpdateExecution({ pipeline, execution })
const title = `${startCase(camelCase(data?.name!.replace(/-/g, ' ')))} Settings`
useEffect(() => {
if (data) {
setName(data.name)
setDesc(data.desc)
}
}, [data])
const handleUpdate = async ({ name, desc }: ExecutionProps) => {
try {
await updateExecution({ name, desc })
showSuccess(getString('common.itemUpdated'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error(err)
}
}
const handleDelete = async () => {
try {
await deleteExecution(execution)
history.push(routes.toPipeline({ pipeline }))
} catch (err) {
showError(`Error: ${err}`)
console.error(err)
}
}
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 handleDelete()
showSuccess(getString('common.itemDeleted'))
refetch()
} catch (err) {
showError(`Error: ${JSON.stringify(err)}`)
console.error({ err })
}
}
}
})
const handleSubmit = (data: ExecutionProps): void => {
handleUpdate(data)
}
return (
<Settings
name={name}
desc={desc}
handleDelete={openDialog}
loading={loading}
handleSubmit={handleSubmit}
refetch={refetch}
title={title}
error={error}
/>
)
}

View File

@ -1,30 +0,0 @@
.root {
display: flex;
flex-direction: column;
flex: 1 auto;
.filterTab {
text-align: center;
padding: 21px;
border-bottom: 3px solid transparent;
&.selected {
border-bottom-color: var(--primary-7);
}
&:hover {
text-decoration: none;
}
}
.header {
padding: var(--spacing-large) var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200);
background: var(--white) !important;
.headerLayout {
align-items: center;
justify-content: flex-end;
}
}
}

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 filterTab: string
readonly header: string
readonly headerLayout: string
readonly root: string
readonly selected: string
}
export default styles

View File

@ -1,118 +0,0 @@
import React from 'react'
import { useParams, useHistory } from 'react-router-dom'
import {
Button,
ButtonVariation,
Container,
Layout,
Page,
useModalHook,
Formik,
FormikForm,
FormInput,
useToaster
} from '@harness/uicore'
import { Dialog } from '@blueprintjs/core'
import { useListExecutions, useCreateExecution, useDeleteExecution } from 'services/pm'
import { startCase, camelCase } from 'lodash'
import { useStrings } from 'framework/strings'
import Table from '../../components/Table/Table'
import routes from 'RouteDefinitions'
import styles from './Executions.module.scss'
export interface ExecutionsParams {
pipeline: string
}
interface ExecutionForm {
name: string
desc: string
}
export const Executions: React.FC = () => {
const history = useHistory()
const { getString } = useStrings()
const { showSuccess, showError } = useToaster()
const { pipeline } = useParams<ExecutionsParams>()
const { mutate: deleteExecution } = useDeleteExecution({ pipeline: pipeline })
const { mutate: createExecution } = useCreateExecution({ pipeline })
const { data: executionList, loading, error, refetch } = useListExecutions({ pipeline })
const title = `${startCase(camelCase(pipeline.replace(/-/g, ' ')))} ${getString('executions')}`
const handleCreate = async ({ name, desc }: ExecutionForm) => {
try {
await createExecution({ name, desc })
showSuccess(getString('common.itemCreated'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error({ error })
}
}
const modalProps = {
isOpen: true,
usePortal: true,
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
title: getString('addExecution'),
style: { width: 400, height: 300 }
}
const handleSubmit = (data: ExecutionForm): void => {
handleCreate(data)
hideModal()
}
const onRowClick = (execution: string) => {
history.push(routes.toPipelineExecutionSettings({ pipeline, execution }))
}
const onSettingsClick = (execution: string) => {
history.push(routes.toPipelineExecutionSettings({ pipeline, execution }))
}
const [openModal, hideModal] = useModalHook(() => (
<Dialog onClose={hideModal} {...modalProps}>
<Container margin={{ top: 'large' }} flex={{ alignItems: 'center', justifyContent: 'space-around' }}>
<Formik<ExecutionForm>
initialValues={{ name: '', desc: '' }}
formName="newExecutionForm"
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text name="name" label={getString('common.name')} />
<FormInput.Text name="desc" label={getString('common.description')} />
<Button type="submit" intent="primary" width="100%">
Create
</Button>
</FormikForm>
</Formik>
</Container>
</Dialog>
))
return (
<Container className={styles.root} height="inherit">
<Page.Header title={title} />
<Layout.Horizontal spacing="large" className={styles.header}>
<Button variation={ButtonVariation.PRIMARY} text="New Execution" icon="plus" onClick={openModal} />
<div style={{ flex: 1 }} />
</Layout.Horizontal>
<Page.Body
loading={loading}
retryOnError={() => refetch()}
error={(error?.data as Error)?.message || error?.message}>
<Table
onRowClick={onRowClick}
refetch={refetch}
data={executionList}
onDelete={deleteExecution}
onSettingsClick={onSettingsClick}
/>
</Page.Body>
</Container>
)
}

View File

@ -12,7 +12,7 @@ import {
Layout,
useToaster
} from '@harness/uicore'
import { get } from 'lodash'
import { get } from 'lodash-es'
import { useAPIToken } from 'hooks/useAPIToken'
import { useOnLogin, useOnRegister } from 'services/pm'
import { useStrings } from 'framework/strings'
@ -43,23 +43,21 @@ export const Login: React.FC = () => {
if (pathname === '/login') {
mutate(formData as unknown as void)
.then(data => {
setToken(get(data, 'access_token' as string))
history.replace(routes.toPipelines())
.then(_data => {
setToken(get(_data, 'access_token' as string))
history.replace(routes.toPolicyDashboard())
})
.catch(error => {
showError(`Error: ${error}`)
console.error({ error })
})
} else {
mutateRegister(formData as unknown as void)
.then(data => {
setToken(get(data, 'access_token' as string))
history.replace(routes.toPipelines())
.then(_data => {
setToken(get(_data, 'access_token' as string))
history.replace(routes.toPolicyDashboard())
})
.catch(error => {
showError(`Error: ${error}`)
console.error({ error })
})
}
}
@ -76,7 +74,7 @@ export const Login: React.FC = () => {
<HarnessLogo height={25} />
</Container>
<Text font={{ size: 'large', weight: 'bold' }} color={Color.BLACK}>
{pathname === '/login' ? getString('signin') : getString('signUp')}
{pathname === '/login' ? getString('signIn') : getString('signUp')}
</Text>
<Text font={{ size: 'medium' }} color={Color.BLACK} margin={{ top: 'xsmall' }}>
and get ship done.
@ -88,10 +86,10 @@ export const Login: React.FC = () => {
formName="loginPageForm"
onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text name="email" label={getString('common.email')} />
<FormInput.Text name="email" label={getString('email')} />
<FormInput.Text name="password" label={getString('password')} inputGroup={{ type: 'password' }} />
<Button type="submit" intent="primary" width="100%">
{pathname === '/login' ? getString('signin') : getString('signUp')}
{pathname === '/login' ? getString('signIn') : getString('signUp')}
</Button>
</FormikForm>
</Formik>
@ -99,8 +97,8 @@ export const Login: React.FC = () => {
<Layout.Horizontal margin={{ top: 'xxxlarge' }} spacing="xsmall">
<Text>{pathname === '/login' ? getString('noAccount') : getString('existingAccount')}</Text>
<Link to={pathname === '/login' ? routes.toRegister() : routes.toLogin()}>
{pathname === '/login' ? getString('signUp') : getString('signin')}
<Link to={pathname === '/login' ? routes.toRegister() : routes.toSignIn()}>
{pathname === '/login' ? getString('signUp') : getString('signIn')}
</Link>
</Layout.Horizontal>
</div>

View File

@ -1,99 +0,0 @@
import React, { useState, useEffect } from 'react'
import { useParams, useHistory } from 'react-router-dom'
import { startCase, camelCase } from 'lodash'
import { useToaster, useConfirmationDialog, Text, Color } from '@harness/uicore'
import { Intent } from '@blueprintjs/core'
import { useStrings } from 'framework/strings'
import { useGetPipeline, useUpdatePipeline, useDeletePipeline } from 'services/pm'
import { Settings } from '../../components/Settings/Settings'
import routes from 'RouteDefinitions'
interface PathProps {
pipeline: string
}
interface PipelineProps {
name?: string
desc?: string
}
export const PipelineSettings = () => {
const history = useHistory()
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const { pipeline } = useParams<PathProps>()
const [name, setName] = useState<string | undefined>('')
const [desc, setDesc] = useState<string | undefined>('')
const { data, loading, error, refetch } = useGetPipeline({ pipeline })
const { mutate: updatePipeline } = useUpdatePipeline({ pipeline })
const { mutate: deletePipeline } = useDeletePipeline({})
const title = `${startCase(camelCase(data?.name!.replace(/-/g, ' ')))} Settings`
useEffect(() => {
if (data) {
setName(data.name)
setDesc(data.desc)
}
}, [data])
const handleUpdate = async ({ name, desc }: PipelineProps) => {
try {
await updatePipeline({ name, desc })
showSuccess(getString('common.itemUpdated'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error(err)
}
}
const handleDelete = async () => {
try {
await deletePipeline(pipeline)
history.push(routes.toPipelines())
} catch (err) {
showError(`Error: ${err}`)
console.error(err)
}
}
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 handleDelete()
showSuccess(getString('common.itemDeleted'))
refetch()
} catch (err) {
showError(`Error: ${JSON.stringify(err)}`)
console.error({ err })
}
}
}
})
const handleSubmit = (data: PipelineProps): void => {
handleUpdate(data)
}
return (
<Settings
name={name}
desc={desc}
handleDelete={openDialog}
loading={loading}
handleSubmit={handleSubmit}
refetch={refetch}
title={title}
error={error}
/>
)
}

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 filterTab: string
readonly header: string
readonly headerLayout: string
readonly root: string
readonly selected: string
}
export default styles

View File

@ -1,112 +0,0 @@
import React from 'react'
import { useHistory } from 'react-router-dom'
import {
Button,
ButtonVariation,
Container,
Layout,
Page,
useModalHook,
Formik,
FormikForm,
FormInput,
useToaster
} from '@harness/uicore'
import { Dialog } from '@blueprintjs/core'
import { useListPipelines, useCreatePipeline, useDeletePipeline } from 'services/pm'
import { useStrings } from 'framework/strings'
import Table from '../../components/Table/Table'
import routes from 'RouteDefinitions'
import styles from './Pipelines.module.scss'
interface PipelineForm {
name: string
desc: string
}
export const Home: React.FC = () => {
const { getString } = useStrings()
const history = useHistory()
const { showError, showSuccess } = useToaster()
const { mutate: createPipeline } = useCreatePipeline({})
const { mutate: deletePipeline } = useDeletePipeline({})
const { data: pipelineList, loading, error, refetch } = useListPipelines({})
const modalProps = {
isOpen: true,
usePortal: true,
autoFocus: true,
canEscapeKeyClose: true,
canOutsideClickClose: true,
enforceFocus: true,
title: 'Add New Pipeline',
style: { width: 400, height: 300 }
}
const onRowClick = (pipeline: string) => {
history.push(routes.toPipeline({ pipeline }))
}
const onSettingsClick = (pipeline: string) => {
history.push(routes.toPipelineSettings({ pipeline }))
}
const [openModal, hideModal] = useModalHook(() => (
<Dialog onClose={hideModal} {...modalProps}>
<Container margin={{ top: 'large' }} flex={{ alignItems: 'center', justifyContent: 'space-around' }}>
<Formik<PipelineForm> initialValues={{ name: '', desc: '' }} formName="newPipelineForm" onSubmit={handleSubmit}>
<FormikForm>
<FormInput.Text name="name" label={getString('common.name')} />
<FormInput.Text name="desc" label={getString('common.description')} />
<Button type="submit" intent="primary" width="100%">
Create
</Button>
</FormikForm>
</Formik>
</Container>
</Dialog>
))
const handleCreate = async (data: PipelineForm) => {
const { name, desc } = data
try {
await createPipeline({ name, desc })
showSuccess(getString('common.itemCreated'))
refetch()
} catch (err) {
showError(`Error: ${err}`)
console.error({ err })
}
}
const handleSubmit = (data: PipelineForm): void => {
handleCreate(data)
hideModal()
}
if (error) {
history.push(routes.toLogin())
}
return (
<Container className={styles.root} height="inherit">
<Page.Header title={getString('pipelines')} />
<Layout.Horizontal spacing="large" className={styles.header}>
<Button variation={ButtonVariation.PRIMARY} text="New Pipeline" icon="plus" onClick={openModal} />
<div style={{ flex: 1 }} />
</Layout.Horizontal>
<Page.Body
loading={loading}
retryOnError={() => refetch()}
error={(error?.data as Error)?.message || error?.message}>
<Table
onRowClick={onRowClick}
refetch={refetch}
data={pipelineList}
onDelete={deletePipeline}
onSettingsClick={onSettingsClick}
/>
</Page.Body>
</Container>
)
}

View File

@ -1,30 +0,0 @@
.root {
display: flex;
flex-direction: column;
flex: 1 auto;
.filterTab {
text-align: center;
padding: 21px;
border-bottom: 3px solid transparent;
&.selected {
border-bottom-color: var(--primary-7);
}
&:hover {
text-decoration: none;
}
}
.header {
padding: var(--spacing-large) var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200);
background: var(--white) !important;
.headerLayout {
align-items: center;
justify-content: flex-end;
}
}
}

View File

@ -1,94 +1,91 @@
import React, { useRef, useState, useCallback } from "react";
import React, { useRef, useState, useCallback } from 'react'
import { useHistory } from 'react-router-dom'
import styles from "./Register.module.scss";
import { useOnRegister } from 'services/pm'
import routes from 'RouteDefinitions'
import Link from "../../components/Link/Link";
import Input from "../../components/Input/input";
import Button from "../../components/Button/button";
import logo from "../../logo.svg"
import Link from '../../components/Link/Link'
import Input from '../../components/Input/input'
import Button from '../../components/Button/button'
import logo from '../../logo.svg'
import styles from './Register.module.scss'
// Renders the Register page.
export const Register = () => {
const history = useHistory()
const [error, setError] = useState(null);
const [fullname, setFullname] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const { mutate } = useOnRegister({})
const history = useHistory()
const [error, setError] = useState(null)
const [fullname, setFullname] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const { mutate } = useOnRegister({})
const handleRegister = useCallback(() => {
const formData = new FormData()
formData.append("fullname", fullname);
formData.append("password", password);
formData.append("username", username);
mutate(formData)
.then(() => {
history.replace(routes.toLogin())
})
.catch(error => {
// TODO: Use toaster to show error
// eslint-disable-next-line no-console
console.error({ error })
setError(error);
})
}, [mutate, username, password, fullname, history])
const handleRegister = useCallback(() => {
const formData = new FormData()
const alert =
error && error.message ? (
<div class="alert">{error.message}</div>
) : undefined;
formData.append('fullname', fullname)
formData.append('password', password)
formData.append('username', username)
return (
<div className={styles.root}>
<div className={styles.logo}>
<img src={logo} />
</div>
<h2>Sign up for a new account</h2>
{alert}
<div className={styles.field}>
<label>Full Name</label>
<Input
type="text"
name="fullname"
placeholder="Full Name"
className={styles.input}
onChange={e => setFullname(e.target.value)}
/>
</div>
<div className={styles.field}>
<label>Email</label>
<Input
type="text"
name="username"
placeholder="Email"
className={styles.input}
onChange={e => setUsername(e.target.value)}
/>
</div>
<div className={styles.field}>
<label>Password</label>
<Input
type="password"
name="password"
placeholder="Password"
className={styles.input}
onChange={e => setPassword(e.target.value)}
/>
</div>
<div>
<Button onClick={handleRegister} className={styles.submit}>
Sign Up
</Button>
</div>
<div className={styles.actions}>
<span>
Already have an account? <Link href="/login">Sign In</Link>
</span>
</div>
</div>
);
}
mutate(formData)
.then(() => {
history.replace(routes.toLogin())
})
.catch(error => {
// TODO: Use toaster to show error
// eslint-disable-next-line no-console
console.error({ error })
setError(error)
})
}, [mutate, username, password, fullname, history])
const alert = error && error.message ? <div class="alert">{error.message}</div> : undefined
return (
<div className={styles.root}>
<div className={styles.logo}>
<img src={logo} />
</div>
<h2>Sign up for a new account</h2>
{alert}
<div className={styles.field}>
<label>Full Name</label>
<Input
type="text"
name="fullname"
placeholder="Full Name"
className={styles.input}
onChange={e => setFullname(e.target.value)}
/>
</div>
<div className={styles.field}>
<label>Email</label>
<Input
type="text"
name="username"
placeholder="Email"
className={styles.input}
onChange={e => setUsername(e.target.value)}
/>
</div>
<div className={styles.field}>
<label>Password</label>
<Input
type="password"
name="password"
placeholder="Password"
className={styles.input}
onChange={e => setPassword(e.target.value)}
/>
</div>
<div>
<Button onClick={handleRegister} className={styles.submit}>
Sign Up
</Button>
</div>
<div className={styles.actions}>
<span>
Already have an account? <Link href="/login">Sign In</Link>
</span>
</div>
</div>
)
}

View File

@ -34,7 +34,7 @@ export const SignIn: React.FC = () => {
return (
<Layout.Vertical>
<h1>{getString('signin')}</h1>
<h1>{getString('signIn')}</h1>
<Container>
<Layout.Horizontal>
<Text>Username</Text>
@ -55,7 +55,7 @@ export const SignIn: React.FC = () => {
</Layout.Horizontal>
</Container>
<Container>
<Button text={getString('signin')} onClick={() => onLogin()} />
<Button text={getString('signIn')} onClick={() => onLogin()} />
</Container>
</Layout.Vertical>
)

View File

@ -5,7 +5,7 @@ export const getConfig = (str: string): string => {
// NOTE: Replace /^pm\// with your service prefixes when running in standalone mode
// I.e: 'pm/api/v1' -> 'api/v1' (standalone)
// -> 'pm/api/v1' (embedded inside Harness platform)
if (window.APP_RUN_IN_STANDALONE_MODE) {
if (window.STRIP_SCM_PREFIX) {
str = str.replace(/^pm\//, '')
}

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'
}
}

View File

@ -7,7 +7,7 @@ import { queryByAttribute } from '@testing-library/react'
import { compile } from 'path-to-regexp'
import { createMemoryHistory } from 'history'
import { Router, Route, Switch, useLocation, useHistory } from 'react-router-dom'
import { ModalProvider } from '@harness/uicore'
import { ModalProvider } from '@harness/use-modal'
import qs from 'qs'
import { enableMapSet } from 'immer'
import { StringsContext } from 'framework/strings'

File diff suppressed because it is too large Load Diff