diff --git a/ui/src/app/settings/components/project-details/project-details.scss b/ui/src/app/settings/components/project-details/project-details.scss new file mode 100644 index 0000000000000..fb7fe46f7d42f --- /dev/null +++ b/ui/src/app/settings/components/project-details/project-details.scss @@ -0,0 +1,20 @@ +@import 'node_modules/argo-ui/src/styles/config'; + +.project-details { + .white-box__details-row .fa-times { + position: absolute; + top: 1em; + right: 0; + cursor: pointer; + } + + .select.argo-field { + padding: 0; + } + + .white-box { + .help-tip { + color: $argo-color-gray-6; + } + } +} \ No newline at end of file diff --git a/ui/src/app/settings/components/project-details/project-details.tsx b/ui/src/app/settings/components/project-details/project-details.tsx index 68870d117af37..09888980278b1 100644 --- a/ui/src/app/settings/components/project-details/project-details.tsx +++ b/ui/src/app/settings/components/project-details/project-details.tsx @@ -1,14 +1,15 @@ -import {NotificationsApi, NotificationType, SlidingPanel, Tabs, Tooltip} from 'argo-ui'; +import {AutocompleteField, FormField, HelpIcon, NotificationsApi, NotificationType, SlidingPanel, Tabs, Tooltip} from 'argo-ui'; +import classNames from 'classnames'; +import * as PropTypes from 'prop-types'; import * as React from 'react'; -import {FormApi} from 'react-form'; +import {FormApi, Text} from 'react-form'; import {RouteComponentProps} from 'react-router'; -import {DataLoader, ErrorNotification, Page, Query} from '../../../shared/components'; -import {Consumer} from '../../../shared/context'; -import {Project} from '../../../shared/models'; +import {CheckboxField, DataLoader, EditablePanel, ErrorNotification, Page, Query} from '../../../shared/components'; +import {AppContext, Consumer} from '../../../shared/context'; +import {Groups, Project, ResourceKinds} from '../../../shared/models'; import {CreateJWTTokenParams, DeleteJWTTokenParams, ProjectRoleParams, services} from '../../../shared/services'; -import {ProjectEditPanel} from '../project-edit-panel/project-edit-panel'; import {ProjectEvents} from '../project-events/project-events'; import {ProjectRoleEditPanel} from '../project-role-edit-panel/project-role-edit-panel'; @@ -18,10 +19,16 @@ import {ProjectSyncWindowsParams} from '../../../shared/services/projects-servic import {SyncWindowStatusIcon} from '../../../applications/components/utils'; +require('./project-details.scss'); + interface ProjectDetailsState { token: string; } +function removeEl(items: any[], index: number) { + return items.slice(0, index).concat(items.slice(index + 1)); +} + function helpTip(text: string) { return ( @@ -33,8 +40,14 @@ function helpTip(text: string) { ); } +function emptyMessage(title: string) { + return

Project has no {title}

; +} + export class ProjectDetails extends React.Component, ProjectDetailsState> { - private projectFormApi: FormApi; + public static contextTypes = { + apis: PropTypes.object + }; private projectRoleFormApi: FormApi; private projectSyncWindowsFormApi: FormApi; private loader: DataLoader; @@ -54,7 +67,6 @@ export class ProjectDetails extends React.Component ctx.navigation.goto('.', {edit: true})}, {title: 'Add Role', iconClassName: 'fa fa-plus', action: () => ctx.navigation.goto('.', {newRole: true})}, {title: 'Add Sync Window', iconClassName: 'fa fa-plus', action: () => ctx.navigation.goto('.', {newWindow: true})}, { @@ -82,7 +94,7 @@ export class ProjectDetails extends React.Component ( {params => ( -
+
ctx.navigation.goto('.', {tab})} @@ -110,56 +122,6 @@ export class ProjectDetails extends React.Component - ctx.navigation.goto('.', {edit: null})} - header={ -
- {' '} - -
- }> - {params.get('edit') === 'true' && ( - (this.projectFormApi = api)} - submit={async projParams => { - try { - await services.projects.update(projParams); - ctx.navigation.goto('.', {edit: null}); - this.loader.reload(); - } catch (e) { - ctx.notifications.show({ - content: , - type: NotificationType.Error - }); - } - }} - /> - )} -
, + type: NotificationType.Error + }); + } + } + private summaryTab(proj: Project) { - const attributes = [{title: 'NAME', value: proj.metadata.name}, {title: 'DESCRIPTION', value: proj.spec.description}]; return (
-
-
- {attributes.map(attr => ( -
-
{attr.title}
-
{attr.value}
-
- ))} -
-
+ this.saveProject(item)} + validate={input => ({ + 'metadata.name': !input.metadata.name && 'Project name is required' + })} + values={proj} + title='GENERAL' + items={[ + { + title: 'NAME', + view: proj.metadata.name, + edit: (_: FormApi) => proj.metadata.name + }, + { + title: 'DESCRIPTION', + view: proj.spec.description, + edit: (formApi: FormApi) => + } + ]} + /> -

Source repositories {helpTip('Git repositories where application manifests are permitted to be retrieved from')}

- {((proj.spec.sourceRepos || []).length > 0 && ( -
-
-
-
URL
-
-
- {(proj.spec.sourceRepos || []).map(src => ( -
-
-
{src}
-
-
- ))} -
- )) || ( -
-

Project has no source repositories

-
- )} - -

Destinations {helpTip('Cluster and namespaces where applications are permitted to be deployed to')}

- {((proj.spec.destinations || []).length > 0 && ( -
-
-
-
SERVER
-
NAMESPACE
-
-
- {(proj.spec.destinations || []).map(dst => ( -
-
-
{dst.server}
-
{dst.namespace}
-
-
- ))} -
- )) || ( -
-

Project has no destinations

-
- )} + this.saveProject(item)} + values={proj} + title={SOURCE REPOSITORIES {helpTip('Git repositories where application manifests are permitted to be retrieved from')}} + view={ + + {proj.spec.sourceRepos + ? proj.spec.sourceRepos.map((repo, i) => ( +
+
{repo}
+
+ )) + : emptyMessage('source repositories')} +
+ } + edit={formApi => ( + services.repos.list()}> + {repos => ( + + {(formApi.values.spec.sourceRepos || []).map((_: Project, i: number) => ( +
+
+ repo.repo)}} + /> + formApi.setValue('spec.sourceRepos', removeEl(formApi.values.spec.sourceRepos, i))} /> +
+
+ ))} + +
+ )} +
+ )} + items={[]} + /> -

Whitelisted cluster resources {helpTip('Cluster-scoped K8s API Groups and Kinds which are permitted to be deployed')}

- {((proj.spec.clusterResourceWhitelist || []).length > 0 && ( -
-
-
-
GROUP
-
KIND
-
-
- {(proj.spec.clusterResourceWhitelist || []).map(res => ( -
-
-
{res.group}
-
{res.kind}
-
-
- ))} -
- )) || ( -
-

No cluster-scoped resources are permitted to deploy

-
- )} + this.saveProject(item)} + values={proj} + title={DESTINATIONS {helpTip('Cluster and namespaces where applications are permitted to be deployed to')}} + view={ + + {proj.spec.destinations ? ( + +
+
Server
+
Namespace
+
+ {proj.spec.destinations.map((dest, i) => ( +
+
{dest.server}
+
{dest.namespace}
+
+ ))} +
+ ) : ( + emptyMessage('destinations') + )} +
+ } + edit={formApi => ( + services.clusters.list()}> + {clusters => ( + +
+
Server
+
Namespace
+
+ {(formApi.values.spec.destinations || []).map((_: Project, i: number) => ( +
+
+ cluster.server)}} + /> +
+
+ +
+ formApi.setValue('spec.destinations', removeEl(formApi.values.spec.destinations, i))} /> +
+ ))} + +
+ )} +
+ )} + items={[]} + /> -

Blacklisted cluster resources {helpTip('Cluster-scoped K8s API Groups and Kinds which are not permitted to be deployed')}

- {((proj.spec.clusterResourceBlacklist || []).length > 0 && ( -
-
-
-
GROUP
-
KIND
+ this.saveProject(item)} + values={proj} + title={CLUSTER RESOURCE ALLOW LIST {helpTip('Cluster-scoped K8s API Groups and Kinds which are permitted to be deployed')}} + view={ + + {proj.spec.clusterResourceWhitelist ? ( + +
+
Kind
+
Group
+
+ {proj.spec.clusterResourceWhitelist.map((resource, i) => ( +
+
{resource.kind}
+
{resource.group}
+
+ ))} +
+ ) : ( + emptyMessage('cluster resource allow list') + )} +
+ } + edit={formApi => ( + +
+
Kind
+
Group
-
- {(proj.spec.clusterResourceBlacklist || []).map(res => ( -
-
-
{res.group}
-
{res.kind}
+ {(formApi.values.spec.clusterResourceWhitelist || []).map((_: Project, i: number) => ( +
+
+ +
+
+ +
+ formApi.setValue('spec.clusterResourceWhitelist', removeEl(formApi.values.spec.clusterResourceWhitelist, i))} + />
+ ))} + + + )} + items={[]} + /> + this.saveProject(item)} + values={proj} + title={CLUSTER RESOURCE DENY LIST {helpTip('Cluster-scoped K8s API Groups and Kinds which are not permitted to be deployed')}} + view={ + + {proj.spec.clusterResourceBlacklist ? ( + +
+
Kind
+
Group
+
+ {proj.spec.clusterResourceBlacklist.map((resource, i) => ( +
+
{resource.kind}
+
{resource.group}
+
+ ))} +
+ ) : ( + emptyMessage('cluster resource deny list') + )} +
+ } + edit={formApi => ( + +
+
Kind
+
Group
- ))} -
- )) || ( -
-

No cluster-scoped resources are not permitted to deploy

-
- )} - -

Blacklisted namespaced resources {helpTip('Namespace-scoped K8s API Groups and Kinds which are prohibited from being deployed')}

- {((proj.spec.namespaceResourceBlacklist || []).length > 0 && ( -
-
-
-
GROUP
-
KIND
-
-
- {(proj.spec.namespaceResourceBlacklist || []).map(res => ( -
-
-
{res.group}
-
{res.kind}
+ {(formApi.values.spec.clusterResourceBlacklist || []).map((_: Project, i: number) => ( +
+
+ +
+
+ +
+ formApi.setValue('spec.clusterResourceBlacklist', removeEl(formApi.values.spec.clusterResourceBlacklist, i))} + />
-
- ))} -
- )) || ( -
-

All namespaced-scoped resources are permitted to deploy

-
- )} + ))} + + + )} + items={[]} + /> + this.saveProject(item)} + values={proj} + title={NAMESPACE RESOURCE ALLOW LIST {helpTip('Namespace-scoped K8s API Groups and Kinds which are permitted to deploy')}} + view={ + + {proj.spec.namespaceResourceWhitelist ? ( + +
+
Kind
+
Group
+
+ {proj.spec.namespaceResourceWhitelist.map((resource, i) => ( +
+
{resource.kind}
+
{resource.group}
+
+ ))} +
+ ) : ( + emptyMessage('namespace resource allow list') + )} +
+ } + edit={formApi => ( + services.clusters.list()}> + {clusters => ( + +
+
Kind
+
Group
+
+ {(formApi.values.spec.namespaceResourceWhitelist || []).map((_: Project, i: number) => ( +
+
+ +
+
+ +
+ formApi.setValue('spec.namespaceResourceWhitelist', removeEl(formApi.values.spec.namespaceResourceWhitelist, i))} + /> +
+ ))} + +
+ )} +
+ )} + items={[]} + /> + this.saveProject(item)} + values={proj} + title={ + + NAMESPACE RESOURCE DENY LIST {helpTip('Namespace-scoped K8s API Groups and Kinds which are prohibited from being deployed')} + + } + view={ + + {proj.spec.namespaceResourceBlacklist ? ( + +
+
Kind
+
Group
+
+ {proj.spec.namespaceResourceBlacklist.map((resource, i) => ( +
+
{resource.kind}
+
{resource.group}
+
+ ))} +
+ ) : ( + emptyMessage('namespace resource deny list') + )} +
+ } + edit={formApi => ( + services.clusters.list()}> + {clusters => ( + +
+
Kind
+
Group
+
+ {(formApi.values.spec.namespaceResourceBlacklist || []).map((_: Project, i: number) => ( +
+
+ +
+
+ +
+ formApi.setValue('spec.namespaceResourceBlacklist', removeEl(formApi.values.spec.namespaceResourceBlacklist, i))} + /> +
+ ))} + +
+ )} +
+ )} + items={[]} + /> -

Whitelisted namespaced resources {helpTip('Namespace-scoped K8s API Groups and Kinds which are permitted to deploy')}

- {((proj.spec.namespaceResourceWhitelist || []).length > 0 && ( -
-
-
-
GROUP
-
KIND
-
-
- {(proj.spec.namespaceResourceWhitelist || []).map(res => ( -
-
-
{res.group}
-
{res.kind}
-
-
- ))} -
- )) || ( -
-

All namespaced-scoped resources are permitted to deploy

-
- )} + this.saveProject(item)} + values={proj} + title={GPG SIGNATURE KEYS {helpTip('IDs of GnuPG keys that commits must be signed with in order to be allowed to sync to')}} + view={ + + {proj.spec.signatureKeys + ? proj.spec.signatureKeys.map((key, i) => ( +
+
{key.keyID}
+
+ )) + : emptyMessage('signature keys')} +
+ } + edit={formApi => ( + services.gpgkeys.list()}> + {keys => ( + + {(formApi.values.spec.signatureKeys || []).map((_: Project, i: number) => ( +
+
+ key.keyID)}} + /> +
+ formApi.setValue('spec.signatureKeys', removeEl(formApi.values.spec.signatureKeys, i))} /> +
+ ))} + +
+ )} +
+ )} + items={[]} + /> -

Required signature keys {helpTip('IDs of GnuPG keys that commits must be signed with in order to be allowed to sync to')}

- {((proj.spec.signatureKeys || []).length > 0 && ( -
-
-
-
KEY ID
-
-
- {(proj.spec.signatureKeys || []).map(res => ( -
-
-
{res.keyID}
+ this.saveProject(item)} + values={proj} + title={RESOURCE MONITORING {helpTip('Enables monitoring of top level resources in the application target namespace')}} + view={ + proj.spec.orphanedResources ? ( + +

+ Enabled +

+

+ {' '} + Application warning conditions are {proj.spec.orphanedResources.warn ? 'enabled' : 'disabled'}. +

+ {(proj.spec.orphanedResources.ignore || []).length > 0 ? ( + +

Resources Ignore List

+
+
Group
+
Kind
+
Name
+
+ {(proj.spec.orphanedResources.ignore || []).map((resource, i) => ( +
+
{resource.group}
+
{resource.kind}
+
{resource.name}
+
+ ))} +
+ ) : ( + emptyMessage('resource ignore list') + )} +
+ ) : ( +

+ Disabled +

+ ) + } + edit={formApi => + formApi.values.spec.orphanedResources ? ( + + +
+
+ Enable application warning conditions? + +
+
+ +
-
- ))} -
- )) || ( -
-

Commit signatures are not required

-
- )} -

Orphaned resource monitoring {helpTip('Enables monitoring of top level resources in the application target namespace')}

- -
-
- {(proj.spec.orphanedResources && ( -
-
WARN
-
- {((proj.spec.orphanedResources.warn === undefined || proj.spec.orphanedResources.warn) && 'enabled') || 'disabled'} +
+ Resources Ignore List +
-
- )) ||

Orphan resources monitoring is disabled

} -
-
- -

Orphaned resources ignore list {helpTip('Resources that ArgoCD should not report them as orphaned')}

- {(((proj.spec.orphanedResources && proj.spec.orphanedResources.ignore) || []).length > 0 && ( -
-
-
-
GROUP
-
KIND
-
NAME
-
-
- {((proj.spec.orphanedResources && proj.spec.orphanedResources.ignore) || []).map(res => ( -
-
-
{res.group}
-
{res.kind}
-
{res.name}
+
+
Group
+
Kind
+
Name
-
- ))} -
- )) || ( -
-

Resources that ArgoCD should not report them as orphaned

-
- )} + {((formApi.values.spec.orphanedResources.ignore || []).length === 0 &&
Ignore list is empty
) || + formApi.values.spec.orphanedResources.ignore.map((_: Project, i: number) => ( +
+
+ +
+
+ +
+
+ +
+ formApi.setValue('spec.orphanedResources.ignore', removeEl(formApi.values.spec.orphanedResources.ignore, i))} + /> +
+ ))} +
+ + + ) : ( + + ) + } + items={[]} + />
); } diff --git a/ui/src/app/shared/components/editable-panel/editable-panel.tsx b/ui/src/app/shared/components/editable-panel/editable-panel.tsx index af988826976f7..1c01279c2379e 100644 --- a/ui/src/app/shared/components/editable-panel/editable-panel.tsx +++ b/ui/src/app/shared/components/editable-panel/editable-panel.tsx @@ -16,13 +16,15 @@ export interface EditablePanelItem { } export interface EditablePanelProps { - title?: string; + title?: string | React.ReactNode; values: T; validate?: (values: T) => any; save?: (input: T) => Promise; items: EditablePanelItem[]; onModeSwitch?: () => any; noReadonlyMode?: boolean; + view?: string | React.ReactNode; + edit?: (formApi: FormApi) => React.ReactNode; } interface EditablePanelState { @@ -90,6 +92,7 @@ export class EditablePanel extends React.Component {this.props.title &&

{this.props.title}

} {(!this.state.edit && ( + {this.props.view && this.props.view} {this.props.items.map(item => ( {item.before && item.before} @@ -127,6 +130,7 @@ export class EditablePanel extends React.Component validateError={this.props.validate}> {api => ( + {this.props.edit && this.props.edit(api)} {this.props.items.map(item => ( {item.before && item.before} diff --git a/ui/src/app/shared/models.ts b/ui/src/app/shared/models.ts index b89e36282c4ab..dc4efec2f55a4 100644 --- a/ui/src/app/shared/models.ts +++ b/ui/src/app/shared/models.ts @@ -721,3 +721,79 @@ export interface GnuPGPublicKey { } export interface GnuPGPublicKeyList extends ItemsList {} + +// https://kubernetes.io/docs/reference/kubectl/overview/#resource-types + +export const ResourceKinds = [ + 'ANY', + 'Binding', + 'ComponentStatus', + 'ConfigMap', + 'Endpoints', + 'LimitRange', + 'Namespace', + 'Node', + 'PersistentVolumeClaim', + 'PersistentVolume', + 'Pod', + 'PodTemplate', + 'ReplicationController', + 'ResourceQuota', + 'Secret', + 'ServiceAccount', + 'Service', + 'MutatingWebhookConfiguration', + 'ValidatingWebhookConfiguration', + 'CustomResourceDefinition', + 'APIService', + 'ControllerRevision', + 'DaemonSet', + 'Deployment', + 'ReplicaSet', + 'StatefulSet', + 'TokenReview', + 'LocalSubjectAccessReview', + 'SelfSubjectAccessReview', + 'SelfSubjectRulesReview', + 'SubjectAccessReview', + 'HorizontalPodAutoscaler', + 'CronJob', + 'Job', + 'CertificateSigningRequest', + 'Lease', + 'Event', + 'Ingress', + 'NetworkPolicy', + 'PodDisruptionBudget', + 'ClusterRoleBinding', + 'ClusterRole', + 'RoleBinding', + 'Role', + 'PriorityClass', + 'CSIDriver', + 'CSINode', + 'StorageClass', + 'Volume' +]; + +export const Groups = [ + 'admissionregistration.k8s.io', + 'apiextensions.k8s.io', + 'apiregistration.k8s.io', + 'apps', + 'authentication.k8s.io', + 'authorization.k8s.io', + 'autoscaling', + 'batch', + 'certificates.k8s.io', + 'coordination.k8s.io', + 'events.k8s.io', + 'extensions', + 'networking.k8s.io', + 'node.k8s.io', + 'policy', + 'rbac.authorization.k8s.io', + 'scheduling.k8s.io', + 'stable.example.com', + 'storage.k8s.io' +]; diff --git a/ui/src/app/shared/services/projects-service.ts b/ui/src/app/shared/services/projects-service.ts index efaf1bd9ccfb4..e84f4bec7ec71 100644 --- a/ui/src/app/shared/services/projects-service.ts +++ b/ui/src/app/shared/services/projects-service.ts @@ -113,6 +113,13 @@ export class ProjectsService { .then(res => res.body as models.Project); } + public async updateProj(project: models.Project): Promise { + return requests + .put(`/projects/${project.metadata.name}`) + .send({project}) + .then(res => res.body as models.Project); + } + public async update(params: ProjectParams): Promise { const proj = await this.get(params.name); const update = paramsToProj(params);