diff --git a/web/config/moduleFederation.config.js b/web/config/moduleFederation.config.js new file mode 100644 index 000000000..63a537ef9 --- /dev/null +++ b/web/config/moduleFederation.config.js @@ -0,0 +1,48 @@ +const packageJSON = require('../package.json'); +const { pick, omit, mapValues } = require('lodash'); + +/** + * These packages must be stricly shared with exact versions + */ + const ExactSharedPackages = [ + 'react', + 'react-dom', + 'react-router-dom', + '@harness/use-modal', + '@blueprintjs/core', + '@blueprintjs/select', + '@blueprintjs/datetime', + 'restful-react', + '@harness/monaco-yaml', + 'monaco-editor', + 'monaco-editor-core', + 'monaco-languages', + 'monaco-plugin-helpers', + 'react-monaco-editor' + ] + +/** + * @type {import('webpack').ModuleFederationPluginOptions} + */ +module.exports = { + name: 'governance', + filename: 'remoteEntry.js', + library: { + type: 'var', + name: 'governance' + }, + exposes: { + './App': './src/App.tsx', + './EvaluationModal': './src/modals/EvaluationModal/EvaluationModal.tsx', + './PipelineGovernanceView': './src/views/PipelineGovernanceView/PipelineGovernanceView.tsx', + './EvaluationView': './src/views/EvaluationView/EvaluationView.tsx', + './PolicySetWizard': './src/pages/PolicySets/components/PolicySetWizard.tsx' + }, + shared: { + formik: packageJSON.dependencies['formik'], + ...mapValues(pick(packageJSON.dependencies, ExactSharedPackages), version => ({ + singleton: true, + requiredVersion: version + })) + } +}; \ No newline at end of file diff --git a/web/config/webpack.common.js b/web/config/webpack.common.js new file mode 100644 index 000000000..ffd01a146 --- /dev/null +++ b/web/config/webpack.common.js @@ -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: /(? { + it('load the dashboard', () => { + cy.visit('/') + cy.contains('In Effect') + cy.contains('Policy Evaluations') + cy.contains('Failures Recorded') + }) +}) diff --git a/web/cypress/integration/evaluations.spec.js b/web/cypress/integration/evaluations.spec.js new file mode 100644 index 000000000..3bb8c4981 --- /dev/null +++ b/web/cypress/integration/evaluations.spec.js @@ -0,0 +1,9 @@ +// disabling at the moment because of logic around Evaluations tab being removed in standalone mode (account in NG equivalent) + +// describe('evaluations', () => { +// it('load the table', () => { +// cy.visit('/') +// cy.contains('Evaluations').click() +// cy.contains('Policy evaluations are created when policy sets are enforced on your Harness entities.') +// }) +// }) diff --git a/web/cypress/integration/policy.spec.js b/web/cypress/integration/policy.spec.js new file mode 100644 index 000000000..3261564e3 --- /dev/null +++ b/web/cypress/integration/policy.spec.js @@ -0,0 +1,7 @@ +describe('policies', () => { + it('load the table', () => { + cy.visit('/') + cy.contains('Policies').click() + cy.get('[class="TableV2--row TableV2--card TableV2--clickable"]').should('have.length', 12) + }) +}) diff --git a/web/cypress/integration/policyset.spec.js b/web/cypress/integration/policyset.spec.js new file mode 100644 index 000000000..e6291d5f3 --- /dev/null +++ b/web/cypress/integration/policyset.spec.js @@ -0,0 +1,7 @@ +describe('policy sets', () => { + it('load the table', () => { + cy.visit('/') + cy.contains('Policy Set').click() + cy.contains('A harness policy set allows you to group policies and configure where they will be enforced.') + }) +}) diff --git a/web/cypress/videos/dashboard.spec.js.mp4 b/web/cypress/videos/dashboard.spec.js.mp4 new file mode 100644 index 000000000..9c07c7dbd Binary files /dev/null and b/web/cypress/videos/dashboard.spec.js.mp4 differ diff --git a/web/cypress/videos/evaluations.spec.js.mp4 b/web/cypress/videos/evaluations.spec.js.mp4 new file mode 100644 index 000000000..1f2afe082 Binary files /dev/null and b/web/cypress/videos/evaluations.spec.js.mp4 differ diff --git a/web/cypress/videos/policy.spec.js.mp4 b/web/cypress/videos/policy.spec.js.mp4 new file mode 100644 index 000000000..08850c099 Binary files /dev/null and b/web/cypress/videos/policy.spec.js.mp4 differ diff --git a/web/cypress/videos/policyset.spec.js.mp4 b/web/cypress/videos/policyset.spec.js.mp4 new file mode 100644 index 000000000..c47719adf Binary files /dev/null and b/web/cypress/videos/policyset.spec.js.mp4 differ diff --git a/web/dist.go b/web/dist.go index 989a72c99..68b85db6b 100644 --- a/web/dist.go +++ b/web/dist.go @@ -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 diff --git a/web/package.json b/web/package.json index c6e388b47..50613e2ed 100644 --- a/web/package.json +++ b/web/package.json @@ -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", + "@types/testing-library__user-event": "^4.1.1", + "@types/uuid": "^8.3.0", + "@types/yup": "^0.29.0", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", + "@urql/devtools": "^2.0.3", + "@zerollup/ts-transform-paths": "^1.7.18", + "assert": "^2.0.0", + "babel-loader": "^8.2.2", + "cache-loader": "^4.1.0", "case": "^1.6.3", "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", "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" diff --git a/web/restful-react.config.js b/web/restful-react.config.js index 3fe478bc0..adbf41630 100644 --- a/web/restful-react.config.js +++ b/web/restful-react.config.js @@ -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")}` } } } diff --git a/web/src/App.tsx b/web/src/App.tsx index eee735c57..0e4417baf 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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 = props => { const [strings, setStrings] = useState() const [token, setToken] = useAPIToken(apiToken) const getRequestOptions = useCallback((): Partial => { - 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 = props => { return strings ? ( - + = props => { on401() } }}> - + {children ? children : } diff --git a/web/src/AppContext.tsx b/web/src/AppContext.tsx index 310d870bd..9f2b27f05 100644 --- a/web/src/AppContext.tsx +++ b/web/src/AppContext.tsx @@ -13,7 +13,7 @@ const AppContext = React.createContext({ components: {} }) -export const AppContextProvider: React.FC<{ value: AppProps }> = ({ value: initialValue, children }) => { +export const AppContextProvider: React.FC<{ value: AppProps }> = React.memo(({ value: initialValue, children }) => { const [appStates, setAppStates] = useState(initialValue) return ( @@ -27,6 +27,6 @@ export const AppContextProvider: React.FC<{ value: AppProps }> = ({ value: initi {children} ) -} +}) export const useAppContext: () => AppContextProps = () => useContext(AppContext) diff --git a/web/src/AppProps.ts b/web/src/AppProps.ts index a39a33706..6c8ee73b3 100644 --- a/web/src/AppProps.ts +++ b/web/src/AppProps.ts @@ -1,5 +1,9 @@ import type React from 'react' +import type * as History from 'history' +import type { PermissionOptionsMenuButtonProps } from 'components/Permissions/PermissionsOptionsMenuButton' +import type { OverviewChartsWithToggleProps } from 'components/OverviewChartsWithToggle/OverviewChartsWithToggle' import type { LangLocale } from './framework/strings/languageLoader' +import type { FeatureFlagMap, GitFiltersProps } from './utils/GovernanceUtils' /** * AppProps defines an interface for host (parent) and @@ -7,7 +11,6 @@ import type { LangLocale } from './framework/strings/languageLoader' * of the child app to be customized from the parent app. * * Areas of customization: - * * - API token * - Active user * - Active locale (i18n) @@ -59,15 +62,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): Array + useGetSchemaYaml(params: any, deps?: Array): Record + 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 +90,21 @@ export interface AppPropsHook {} // eslint-disable-line @typescript-eslint/no-e */ export interface AppPropsComponent { NGBreadcrumbs: React.FC + RbacButton: React.FC + RbacOptionsMenuButton: React.FC + GitFilters: React.FC + GitSyncStoreProvider: React.FC + GitContextForm: React.FC + NavigationCheck: React.FC<{ + when?: boolean + textProps?: { + contentText?: string + titleText?: string + confirmButtonText?: string + cancelButtonText?: string + } + navigate: (path: string) => void + shouldBlockNavigation?: (location: History.Location) => boolean + }> + OverviewChartsWithToggle: React.FC } diff --git a/web/src/RouteDefinitions.ts b/web/src/RouteDefinitions.ts index 361540bfc..a20413b04 100644 --- a/web/src/RouteDefinitions.ts +++ b/web/src/RouteDefinitions.ts @@ -3,37 +3,99 @@ import type { AppPathProps } from 'AppProps' export enum RoutePath { SIGNIN = '/signin', - TEST_PAGE1 = '/test-page1', - TEST_PAGE2 = '/test-page2', - - 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), - toRegister: (): string => toRouteURL(RoutePath.REGISTER), - toAccount: (): string => toRouteURL(RoutePath.ACCOUNT), - toPipelines: (): string => toRouteURL(RoutePath.PIPELINES), - toPipeline: ({ pipeline }: Required>): string => - toRouteURL(RoutePath.PIPELINE, { pipeline }), - toPipelineExecutions: ({ pipeline }: Required>): string => - toRouteURL(RoutePath.PIPELINE_EXECUTIONS, { pipeline }), - toPipelineSettings: ({ pipeline }: Required>): 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 + toSignIn: (): string => toRouteURL(RoutePath.SIGNIN), + toPolicyDashboard: (): string => toRouteURL(RoutePath.POLICY_DASHBOARD), + toPolicyListing: (): string => toRouteURL(RoutePath.POLICY_LISTING), + toPolicyNew: (): string => toRouteURL(RoutePath.POLICY_NEW), + toPolicyView: ({ policyIdentifier }: Required>): string => + toRouteURL(RoutePath.POLICY_VIEW, { policyIdentifier }), + toPolicyEdit: ({ policyIdentifier }: Required>): 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) => + toRouteURL(RoutePath.POLICY_EDIT, { + orgIdentifier, + projectIdentifier, + policyIdentifier, + module, + repo, + branch + }), + toGovernanceViewPolicy: ({ + orgIdentifier, + projectIdentifier, + policyIdentifier, + module + }: RequireField) => + 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 + }) } diff --git a/web/src/RouteDestinations.tsx b/web/src/RouteDestinations.tsx index 8ef8a7aa2..0f5158cfe 100644 --- a/web/src/RouteDestinations.tsx +++ b/web/src/RouteDestinations.tsx @@ -1,75 +1,105 @@ -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 { routePath, standaloneRoutePath } from './RouteUtils' +import { RoutePath } from './RouteDefinitions' +import PolicyControlPage from './pages/PolicyControl/PolicyControlPage' +import Policies from './pages/Policies/Policies' +import PolicyDashboard from './pages/PolicyDashboard/PolicyDashboard' +import PolicySets from './pages/PolicySets/PolicySets' +import PolicyEvaluations from './pages/PolicyEvaluations/PolicyEvaluations' +import { EditPolicy } from './pages/EditPolicy/EditPolicy' +import { ViewPolicy } from './pages/ViewPolicy/ViewPolicy' +import { PolicySetDetail } from './pages/PolicySetDetail/PolicySetDetail' +import { EvaluationDetail } from './pages/EvaluationDetail/EvaluationDetail' -import { Login } from './pages/Login/Login' -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 }) => { + // TODO: Add Auth wrapper -export const RouteDestinations: React.FC> = ({ standalone }) => { - // TODO: Add a generic Auth Wrapper + const Destinations: React.FC = useCallback( + () => ( + + {standalone && ( + + + + )} - const Destinations: React.FC = () => ( - - {standalone && ( - - - - )} - {standalone && ( - - - - )} + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - ) + + + + + - return standalone ? ( - + + + + + + + + + + + + + + {standalone ? ( + + ) : ( + + )} + + + ), + [standalone] + ) + + return standalone ? ( + + + + ) : ( - - ) : ( - - ) -} + ) + } +) diff --git a/web/src/RouteUtils.ts b/web/src/RouteUtils.ts index e1acc32d0..3da942e57 100644 --- a/web/src/RouteUtils.ts +++ b/web/src/RouteUtils.ts @@ -14,40 +14,42 @@ type Scope = Pick { - 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) diff --git a/web/src/bootstrap.tsx b/web/src/bootstrap.tsx index a4b8a8705..dac29e3ef 100644 --- a/web/src/bootstrap.tsx +++ b/web/src/bootstrap.tsx @@ -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_PM_PREFIX = true -ReactDOM.render(, document.getElementById('react-root')) +ReactDOM.render( + , + document.getElementById('react-root') +) diff --git a/web/src/components/ContainerSpinner/ContainerSpinner.module.scss b/web/src/components/ContainerSpinner/ContainerSpinner.module.scss deleted file mode 100644 index c037cbce3..000000000 --- a/web/src/components/ContainerSpinner/ContainerSpinner.module.scss +++ /dev/null @@ -1,8 +0,0 @@ -.spinner { - width: 100%; - height: 100%; - - > div { - position: relative !important; - } -} diff --git a/web/src/components/ContainerSpinner/ContainerSpinner.tsx b/web/src/components/ContainerSpinner/ContainerSpinner.tsx deleted file mode 100644 index a5b1d7f6e..000000000 --- a/web/src/components/ContainerSpinner/ContainerSpinner.tsx +++ /dev/null @@ -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> = ({ className, ...props }) => { - return ( - - - - ) -} diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss deleted file mode 100644 index 3f6be286e..000000000 --- a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss +++ /dev/null @@ -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; - } -} diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts b/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts deleted file mode 100644 index dc3c2ec71..000000000 --- a/web/src/components/EvaluationStatus/EvaluationStatusLabel.module.scss.d.ts +++ /dev/null @@ -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 diff --git a/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx b/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx deleted file mode 100644 index fec25d7ce..000000000 --- a/web/src/components/EvaluationStatus/EvaluationStatusLabel.tsx +++ /dev/null @@ -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 = ({ - 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 ( - - {label} - - ) -} diff --git a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx index 2c07071bb..9cc767d6b 100644 --- a/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx +++ b/web/src/components/NameIdDescriptionTags/NameIdDescriptionTags.tsx @@ -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 ( @@ -55,9 +55,7 @@ export const Description = (props: DescriptionComponentProps): JSX.Element => { return (