Merge pull request #3 from harness/ui-template-improved

Simplify routing system
This commit is contained in:
Tan Nhu 2022-08-22 16:04:37 -07:00 committed by GitHub
commit 7f5a783416
17 changed files with 203 additions and 993 deletions

View File

@ -6,7 +6,6 @@ import { FocusStyleManager } from '@blueprintjs/core'
import { tooltipDictionary } from '@harness/ng-tooltip'
import AppErrorBoundary from 'framework/AppErrorBoundary/AppErrorBoundary'
import { AppContextProvider } from 'AppContext'
import { setBaseRouteInfo } from 'RouteUtils'
import type { AppProps } from 'AppProps'
import { buildResfulReactRequestOptions, handle401 } from 'AppUtils'
import { RouteDestinations } from 'RouteDestinations'
@ -34,7 +33,6 @@ const App: React.FC<AppProps> = props => {
const getRequestOptions = useCallback((): Partial<RequestInit> => {
return buildResfulReactRequestOptions(hooks.useGetToken?.() || apiToken || 'default')
}, []) // eslint-disable-line react-hooks/exhaustive-deps
setBaseRouteInfo(accountId, baseRoutePath)
useEffect(() => {
languageLoader(lang).then(setStrings)

View File

@ -2,7 +2,6 @@ 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
@ -72,7 +71,6 @@ export interface AppPathProps {
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
@ -91,7 +89,6 @@ 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<{

View File

@ -1,105 +1,19 @@
import { toRouteURL } from 'RouteUtils'
import type { AppPathProps } from 'AppProps'
export enum RoutePath {
// Standalone-only paths
SIGNIN = '/signin',
SIGNUP = '/signup',
REGISTER = '/register',
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'
// Shared paths
DASHBOARD = '/dashboard',
TEST_PAGE1 = '/test1',
TEST_PAGE2 = '/test2'
}
export default {
toSignIn: (): string => toRouteURL(RoutePath.SIGNIN),
toSignUp: (): string => toRouteURL(RoutePath.SIGNUP),
toRegister: (): string => toRouteURL(RoutePath.REGISTER),
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
})
toSignIn: (): string => RoutePath.SIGNIN,
toSignUp: (): string => RoutePath.SIGNUP,
toDashboard: (): string => RoutePath.DASHBOARD,
toTestPage1: (): string => RoutePath.TEST_PAGE1,
toTestPage2: (): string => RoutePath.TEST_PAGE2
}

View File

@ -1,65 +1,43 @@
/* 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 { SignIn } from 'pages/SignIn/SignIn'
import { Register } from 'pages/Register/Register'
import { routePath, standaloneRoutePath } from './RouteUtils'
import { SignUp } from 'pages/SignUp/SignUp'
import { routePath } from './RouteUtils'
import { RoutePath } from './RouteDefinitions'
export const RouteDestinations: React.FC<{ standalone: boolean }> = React.memo(({ standalone }) => {
// console.log({ standalone, signin: routePath(standalone, RoutePath.DASHBOARD) })
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>
</>
<Route path={routePath(standalone, RoutePath.SIGNIN)}>
<SignIn />
</Route>
)}
<Route path={routePath(RoutePath.POLICY_DASHBOARD)}>
<h1>Overview</h1>
{standalone && (
<Route path={routePath(standalone, RoutePath.SIGNUP)}>
<SignUp />
</Route>
)}
<Route path={routePath(standalone, RoutePath.DASHBOARD)}>
<h1>DASHBOARD</h1>
</Route>
<Route path={routePath(RoutePath.POLICY_NEW)}>
<h1>New</h1>
<Route path={routePath(standalone, RoutePath.TEST_PAGE1)}>
<h1>TEST_PAGE1</h1>
</Route>
<Route path={routePath(RoutePath.POLICY_VIEW)}>
<h1>View</h1>
</Route>
<Route exact path={routePath(RoutePath.POLICY_EDIT)}>
<h1>Edit</h1>
</Route>
<Route path={routePath(RoutePath.POLICY_LISTING)}>
<h1>Listing</h1>
</Route>
<Route exact path={routePath(RoutePath.POLICY_SETS_LISTING)}>
<h1>Listing 2</h1>
</Route>
<Route path={routePath(RoutePath.POLICY_SETS_DETAIL)}>
<h1>Detail 1</h1>
</Route>
<Route path={routePath(RoutePath.POLICY_EVALUATION_DETAIL)}>
<h1>Detail 2</h1>
<Route path={routePath(standalone, RoutePath.TEST_PAGE2)}>
<h1>TEST_PAGE2</h1>
</Route>
<Route path="/">
{standalone ? <Redirect to={standaloneRoutePath(RoutePath.POLICY_DASHBOARD)} /> : <NotFoundPage />}
{standalone ? <Redirect to={routePath(standalone, RoutePath.DASHBOARD) as string} /> : <NotFoundPage />}
</Route>
</Switch>
),

View File

@ -1,61 +1,7 @@
import { generatePath } from 'react-router-dom'
import type { AppPathProps } from 'AppProps'
let baseRoutePath: string
let accountId: string
export const setBaseRouteInfo = (_accountId: string, _baseRoutePath: string): void => {
accountId = _accountId
baseRoutePath = _baseRoutePath
}
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/wings-software/nextgenui/blob/master/src/modules/10-common/utils/routeUtils.ts#L171
//
const getScopeBasedRouteURL = ({ path, scope = {} }: { path: string; scope?: Scope }): string => {
const { orgIdentifier, projectIdentifier, module } = scope
// 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/governance${path}`
} else if (orgIdentifier && projectIdentifier) {
return `/account/${accountId}/home/orgs/${orgIdentifier}/projects/${projectIdentifier}/setup/governance${path}`
} else if (orgIdentifier) {
return `/account/${accountId}/settings/organizations/${orgIdentifier}/setup/governance${path}`
}
return `/account/${accountId}/settings/governance${path}`
}
/**
* Generate route paths to be used in RouteDefinitions.
* @param path route path
* @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[] => [
`/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)
* @param path route path
* @param params URL parameters
* @returns a proper URL that works in both standalone and embedded modes.
*/
export const toRouteURL = (path: string, params?: AppPathProps): string =>
generatePath(getScopeBasedRouteURL({ path, scope: params }), { ...params, accountId })
export const routePath = (standalone: boolean, path: string): string | string[] =>
standalone ? path : [`/account/:accountId/scm${path}`]

View File

@ -39,7 +39,7 @@ interface NameIdProps {
export const NameId = (props: NameIdProps): JSX.Element => {
const { getString } = useStrings()
const { identifierProps, nameLabel = getString('name'), inputGroupProps = {} } = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<FormInput.InputWithIdentifier inputLabel={nameLabel} inputGroupProps={newInputGroupProps} {...identifierProps} />
)
@ -156,7 +156,7 @@ function TagsDeprecated(props: TagsDeprecatedComponentProps): JSX.Element {
export function NameIdDescriptionTags(props: NameIdDescriptionTagsProps): JSX.Element {
const { getString } = useStrings()
const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {}, tooltipProps } = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<Container className={cx(css.main, className)}>
<NameId identifierProps={identifierProps} inputGroupProps={newInputGroupProps} />
@ -184,7 +184,7 @@ export function NameIdDescriptionTagsDeprecated<T>(props: NameIdDescriptionTagsD
export function NameIdDescription(props: NameIdDescriptionProps): JSX.Element {
const { getString } = useStrings()
const { className, identifierProps, descriptionProps, formikProps, inputGroupProps = {} } = props
const newInputGroupProps = { placeholder: getString('common.namePlaceholder'), ...inputGroupProps }
const newInputGroupProps = { placeholder: getString('namePlaceholder'), ...inputGroupProps }
return (
<Container className={cx(css.main, className)}>

View File

@ -3,7 +3,7 @@ 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 { useGetTrialInfo } from 'utils/Utils'
import { useStrings } from 'framework/strings'
import css from './TrialBanner.module.scss'

View File

@ -3,53 +3,12 @@
* Use the command `yarn strings` to regenerate this file.
*/
export interface StringsMap {
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.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
@ -57,83 +16,19 @@ export interface StringsMap {
details: string
edit: string
email: string
entity: string
'evaluation.evaluatedPoliciesCount': string
'evaluation.onePolicyEvaluated': string
executionsText: string
existingAccount: 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
namePlaceholder: string
no: string
noAccount: string
noSearchResultsFound: string
optionalField: string
outputLabel: string
overview: string
pageNotFound: string
password: string
samplePolicies: string
saveOverwrite: string
search: string
signIn: string
signUp: string
source: string
@ -141,12 +36,5 @@ export interface StringsMap {
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
}

View File

@ -1,11 +0,0 @@
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

@ -29,131 +29,7 @@ tagsLabel: Tags
yes: Yes
no: No
source: Source
common:
namePlaceholder: Enter Name
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
namePlaceholder: Enter Name
banner:
expired: expired {{ days }} days ago
expiryCountdown: expires in {{ days }} days

View File

@ -45,7 +45,7 @@ export const Login: React.FC = () => {
mutate(formData as unknown as void)
.then(_data => {
setToken(get(_data, 'access_token' as string))
history.replace(routes.toPolicyDashboard())
history.replace(routes.toDashboard())
})
.catch(error => {
showError(`Error: ${error}`)
@ -54,7 +54,7 @@ export const Login: React.FC = () => {
mutateRegister(formData as unknown as void)
.then(_data => {
setToken(get(_data, 'access_token' as string))
history.replace(routes.toPolicyDashboard())
history.replace(routes.toDashboard())
})
.catch(error => {
showError(`Error: ${error}`)
@ -89,16 +89,16 @@ export const Login: React.FC = () => {
<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 === '/signin' ? getString('signIn') : getString('signUp')}
</Button>
</FormikForm>
</Formik>
</Container>
<Layout.Horizontal margin={{ top: 'xxxlarge' }} spacing="xsmall">
<Text>{pathname === '/login' ? getString('noAccount') : getString('existingAccount')}</Text>
<Link to={pathname === '/login' ? routes.toRegister() : routes.toSignIn()}>
{pathname === '/login' ? getString('signUp') : getString('signIn')}
<Text>{pathname === '/signin' ? getString('noAccount') : getString('existingAccount')}</Text>
<Link to={pathname === '/signin' ? routes.toSignUp() : routes.toSignIn()}>
{pathname === '/signin' ? getString('signUp') : getString('signIn')}
</Link>
</Layout.Horizontal>
</div>

View File

@ -1,91 +0,0 @@
import React, { useRef, useState, useCallback } from 'react'
import { useHistory } from 'react-router-dom'
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 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 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 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

@ -0,0 +1,11 @@
import React from 'react'
import styles from './SignUp.module.scss'
// Renders the Register page.
export const SignUp: React.FC = () => {
return (
<div className={styles.root}>
<h2>Sign up for a new account</h2>
</div>
)
}

View File

@ -1,442 +0,0 @@
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])
}

146
web/src/utils/Utils.ts Normal file
View File

@ -0,0 +1,146 @@
import { Intent, IToaster, IToastProps, Position, Toaster } from '@blueprintjs/core'
import type { editor as EDITOR } from 'monaco-editor/esm/vs/editor/editor.api'
import { get } from 'lodash-es'
import moment from 'moment'
import { useEffect } from 'react'
import { useAppContext } from 'AppContext'
/** 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 LIST_FETCHING_PAGE_SIZE = 20
export const DEFAULT_DATE_FORMAT = 'MM/DD/YYYY hh:mm a'
export const displayDateTime = (value: number): string | null => {
return value ? moment.unix(value / 1000).format(DEFAULT_DATE_FORMAT) : null
}
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])
}